From 11e58e3b23e91faeac8aa093eb1c2f4504fba07a Mon Sep 17 00:00:00 2001 From: Bohyun Lee Date: Fri, 31 Jul 2026 14:21:53 +0200 Subject: [PATCH] Add pg_upgrade --wal-upgrade: capture a major-version upgrade as WAL pg_upgrade normally reinitializes the new cluster with a fresh system identifier and WAL history, which breaks LSN continuity at the upgrade boundary: a base backup taken before the upgrade cannot be replayed into the upgraded cluster, and every streaming standby must be rebuilt by copying the whole data directory. --wal-upgrade instead records the upgrade as a replayable WAL "window" so the transformation can be streamed and replayed like ordinary WAL. The upgrade runs as usual, then a burst server started in binary-upgrade mode emits the whole window (CN..COMPLETE) in one gated backend call using a new RM_PG_UPGRADE_ID resource manager: rewritten catalog/system files travel as full-page images (RELFILE_DATA), SLRUs as bulk segment images (SLRU_DATA), non-relation files (pg_filenode.map, PG_VERSION) as raw images (RAWFILE), and the directory skeleton as DIRTREE. User relations, which pg_upgrade transfers verbatim, are not carried as data; their identities are logged in a RELINK manifest so a streaming standby relinks them from its own retained old data directory (reproducing the primary's transfer mode). START/COMPLETE markers plus durable upgrade_started / upgrade_finalized flags in pg_control make a crashed partial upgrade detectable and refused rather than auto-served. On recovery the new binary derives the end-of-upgrade checkpoint (CN) in-process and arms pg_control to replay the window, supporting three paths: local first-start, a fresh streaming-standby skeleton (armed via the pg_upgrade.signal sentinel plus pg_upgrade_standby_old_datadir / pg_upgrade_standby_transfer_mode GUCs), and cross-version archive PITR (pg_control is synthesized so the version gate passes, then recovery flows organically through CN). --wal-upgrade-signal-handoff emits a handoff record into the old primary's WAL so a physical standby pauses cleanly at the boundary. Bumps PG_CONTROL_VERSION and CATALOG_VERSION_NO. Co-authored-by: Isaac --- doc/src/sgml/ref/pgupgrade.sgml | 133 +++ src/backend/access/rmgrdesc/Makefile | 1 + src/backend/access/rmgrdesc/meson.build | 1 + src/backend/access/rmgrdesc/pgupgradedesc.c | 170 ++++ src/backend/access/rmgrdesc/xlogdesc.c | 11 + src/backend/access/transam/Makefile | 3 +- src/backend/access/transam/clog.c | 10 + src/backend/access/transam/meson.build | 1 + src/backend/access/transam/multixact.c | 17 + src/backend/access/transam/pgupgrade_wal.c | 1963 +++++++++++++++++++++++++++++++++++++++++++ src/backend/access/transam/rmgr.c | 1 + src/backend/access/transam/slru.c | 71 ++ src/backend/access/transam/xlog.c | 1289 +++++++++++++++++++++++++++- src/backend/access/transam/xlogfuncs.c | 465 ++++++++++ src/backend/access/transam/xlogrecovery.c | 32 + src/backend/postmaster/postmaster.c | 7 + src/backend/postmaster/startup.c | 11 + src/backend/replication/walsender.c | 1 + src/backend/storage/buffer/bufmgr.c | 25 + src/backend/utils/adt/pg_upgrade_support.c | 30 + src/backend/utils/init/miscinit.c | 86 ++ src/backend/utils/misc/guc_parameters.dat | 15 + src/backend/utils/misc/postgresql.conf.sample | 8 + src/bin/initdb/initdb.c | 6 + src/bin/pg_controldata/pg_controldata.c | 4 + src/bin/pg_ctl/pg_ctl.c | 17 + src/bin/pg_resetwal/pg_resetwal.c | 42 +- src/bin/pg_upgrade/Makefile | 1 + src/bin/pg_upgrade/check.c | 47 ++ src/bin/pg_upgrade/controldata.c | 9 + src/bin/pg_upgrade/file.c | 67 +- src/bin/pg_upgrade/info.c | 87 +- src/bin/pg_upgrade/meson.build | 4 + src/bin/pg_upgrade/option.c | 75 +- src/bin/pg_upgrade/pg_upgrade.c | 790 ++++++++++++++++- src/bin/pg_upgrade/pg_upgrade.h | 55 ++ src/bin/pg_upgrade/relfilenumber.c | 1 + src/bin/pg_upgrade/revertable.c | 129 +++ src/bin/pg_upgrade/server.c | 54 +- src/bin/pg_upgrade/t/009_initdb_option.pl | 218 +++++ src/bin/pg_upgrade/t/010_wal_upgrade.pl | 669 +++++++++++++++ src/bin/pg_upgrade/t/011_wal_upgrade_standby.pl | 753 +++++++++++++++++ src/bin/pg_waldump/pgupgradedesc.c | 1 + src/bin/pg_waldump/rmgrdesc.c | 1 + src/bin/pg_waldump/t/001_basic.pl | 3 +- src/common/file_utils.c | 124 +++ src/include/access/clog.h | 3 + src/include/access/multixact.h | 4 + src/include/access/pgupgrade_wal.h | 98 +++ src/include/access/rmgrlist.h | 2 + src/include/access/slru.h | 4 + src/include/access/xlog.h | 74 ++ src/include/access/xlogrecovery.h | 7 + src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_control.h | 216 ++++- src/include/catalog/pg_proc.dat | 8 + src/include/common/file_utils.h | 17 + src/include/utils/guc.h | 1 + src/tools/pgindent/typedefs.list | 15 + 59 files changed, 7871 insertions(+), 88 deletions(-) diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml index e4e8c02e6d..645eceb04b 100644 --- a/doc/src/sgml/ref/pgupgrade.sgml +++ b/doc/src/sgml/ref/pgupgrade.sgml @@ -262,6 +262,31 @@ PostgreSQL documentation + + + + + Create the new cluster automatically by running + initdb before upgrading, instead of requiring the + user to have created it manually. The WAL segment size, data checksum + setting, encoding, and locale are derived from the old cluster so that + pg_upgrade can verify compatibility. + + + The new cluster data directory specified with + / must not already + exist when this option is given; if it does, + pg_upgrade will exit with an error. + + + This option cannot be combined with + /, because + is read-only and must not create the new + cluster. + + + + @@ -373,6 +398,108 @@ PostgreSQL documentation + + + + + Capture the entire upgrade as write-ahead log (WAL) so that it is + replayable, and leave the new cluster ready to start read-write on its + own. The new cluster comes up on its first start with no separate + commit step, exactly as an ordinary upgraded cluster would. + + + Compared with a plain upgrade, keeps the + old cluster intact so the upgrade can be reverted (see + ) until it is explicitly retired + (see ), and it makes the + upgraded state streamable so a standby can be re-provisioned from the + upgraded primary by ordinary replication rather than a fresh base backup. + + + A replacement standby need not even be initialized with + initdb: an empty data directory containing only its + configuration, a standby.signal, a + pg_upgrade.signal sentinel (whose contents are the + path of this standby's retained pre-upgrade data directory, from which + user relations are linked in), and + primary_conninfo pointing at the upgraded primary is + sufficient. On its first start the server synthesizes the control file + and PG_VERSION, fetches the upgrade anchor from the + primary over the replication connection, and streams the upgrade in, + coming up as a hot standby of the new-version primary. + + + It may be combined with any transfer mode. With + or the old cluster's + files are duplicated, so it stays intact and the upgrade is revertable. + and also work, but they + disable the old cluster (as in an ordinary upgrade), so those upgrades + are forward-only: then has + nothing to revert to. ( shares the old cluster's + files, and moves them into the new cluster.) + + + + + + + + + Discard a cluster produced by and return + to the old cluster, which was never modified. The new cluster data + directory given with / + is removed; the operator then simply restarts on the old data directory + with the old binaries. + + + Rollback is available whenever the old cluster is intact. If the old + cluster is unusable — consumed by , damaged, + or already started after the upgrade — there is nothing safe to + return to, so the command exits with an error. If the new cluster has + already been started and taken writes, rolling back discards them; the + command warns rather than blocks, leaving that judgement to the + operator. + + + + + + + + + Retire the old cluster once the operator is confident in the upgrade. + The old cluster data directory given with + / is removed. This + requires a fully upgraded new cluster + (/), so the old cluster + is never removed unless a working replacement exists. + + + pg_upgrade does not distinguish a primary from + a standby: to clean up a replica set, run this command on each node + whose old cluster should be removed. + + + + + + + + + Emit a handoff trigger into the live old cluster's WAL and then shut the + old cluster down at that point, so that streaming standbys following it + stand down before the upgrade. Shutting the old cluster down as part of + this step ensures no transaction appends WAL after the handoff record, + which would otherwise leave standbys stopping short of the primary's + final state. Requires the old cluster's data directory + (/) and bin directory + (/). This is used before + a run in replicated deployments; it does + not itself upgrade anything. + + + + @@ -462,6 +589,12 @@ make prefix=/usr/local/pgsql.new install prebuilt installers do this step automatically. There is no need to start the new cluster. + + Alternatively, pass to + pg_upgrade to have it run + initdb automatically, deriving the required settings + from the old cluster. In that case this manual step can be skipped. + diff --git a/src/backend/access/rmgrdesc/Makefile b/src/backend/access/rmgrdesc/Makefile index cd95eec37f..601c30dd5a 100644 --- a/src/backend/access/rmgrdesc/Makefile +++ b/src/backend/access/rmgrdesc/Makefile @@ -10,6 +10,7 @@ include $(top_builddir)/src/Makefile.global OBJS = \ brindesc.o \ + pgupgradedesc.o \ clogdesc.o \ committsdesc.o \ dbasedesc.o \ diff --git a/src/backend/access/rmgrdesc/meson.build b/src/backend/access/rmgrdesc/meson.build index d9000ccd9f..5070a526d3 100644 --- a/src/backend/access/rmgrdesc/meson.build +++ b/src/backend/access/rmgrdesc/meson.build @@ -14,6 +14,7 @@ rmgr_desc_sources = files( 'logicalmsgdesc.c', 'mxactdesc.c', 'nbtdesc.c', + 'pgupgradedesc.c', 'relmapdesc.c', 'replorigindesc.c', 'rmgrdesc_utils.c', diff --git a/src/backend/access/rmgrdesc/pgupgradedesc.c b/src/backend/access/rmgrdesc/pgupgradedesc.c new file mode 100644 index 0000000000..6ffe85848b --- /dev/null +++ b/src/backend/access/rmgrdesc/pgupgradedesc.c @@ -0,0 +1,170 @@ +/*------------------------------------------------------------------------- + * + * pgupgradedesc.c + * rmgr descriptor routines for RM_PG_UPGRADE_ID WAL records. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/access/rmgrdesc/pgupgradedesc.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/pgupgrade_wal.h" +#include "access/transam.h" /* FirstNormalObjectId */ +#include "access/xlogreader.h" +#include "catalog/pg_control.h" +#include "lib/stringinfo.h" + +void +pg_upgrade_desc(StringInfo buf, XLogReaderState *record) +{ + char *rec = XLogRecGetData(record); + uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; + + if (info == XLOG_UPGRADE_START || info == XLOG_UPGRADE_COMPLETE) + { + xl_pg_upgrade xlrec; + + memcpy(&xlrec, rec, SizeOfXLPgUpgrade); + appendStringInfo(buf, "old_major_version %u; new_major_version %u; time %lld", + xlrec.old_major_version, + xlrec.new_major_version, + (long long) xlrec.upgrade_time); + } + else if (info == XLOG_UPGRADE_SLRU_DATA) + { + xl_upgrade_slru_data xlrec; + const char *slru_names[] = UPGRADE_SLRU_DIRS; + + memcpy(&xlrec, rec, SizeOfXLUpgradeSlruData); + appendStringInfo(buf, "slru %s; segs %04" PRIX64 "..%04" PRIX64 "; bytes %u", + xlrec.slru_type < 3 ? slru_names[xlrec.slru_type] : "unknown", + xlrec.first_seg, + xlrec.last_seg, + xlrec.total_bytes); + } + else if (info == XLOG_UPGRADE_RELFILE_DATA) + { + /* Batched: [entry][data][entry][data]... Summarize the entries. */ + char *ptr = rec; + char *end = rec + XLogRecGetDataLen(record); + int nentries = 0; + int nuser = 0; + uint64 total = 0; + xl_upgrade_relfile_entry first; + + while (ptr + SizeOfXLUpgradeRelfileEntry <= end) + { + xl_upgrade_relfile_entry ent; + + memcpy(&ent, ptr, SizeOfXLUpgradeRelfileEntry); + if (nentries == 0) + first = ent; + nentries++; + + /* + * User relations are listed in the RELINK manifest, not carried + * as data in a RELFILE record. Count any user relfilenumbers + * that do appear here, so the "user" total in pg_waldump is 0 for + * a schema-only window. + */ + if (ent.relfilenumber >= FirstNormalObjectId) + nuser++; + total += ent.nbytes; + ptr += SizeOfXLUpgradeRelfileEntry + ent.nbytes; + } + appendStringInfo(buf, "%d files (%d user); %llu bytes; first rel %u/%u/%u seg %u blkoff %u", + nentries, nuser, (unsigned long long) total, + nentries ? first.tablespace_oid : 0, + nentries ? first.database_oid : 0, + nentries ? first.relfilenumber : 0, + nentries ? first.segno : 0, + nentries ? first.blockoff : 0); + } + else if (info == XLOG_UPGRADE_RELINK) + { + /* Manifest of user relations to link on the standby: no file data. */ + char *ptr = rec; + char *end = rec + XLogRecGetDataLen(record); + int nentries = 0; + xl_upgrade_relink_entry first; + + while (ptr + SizeOfXLUpgradeRelinkEntry <= end) + { + xl_upgrade_relink_entry ent; + + memcpy(&ent, ptr, SizeOfXLUpgradeRelinkEntry); + if (nentries == 0) + first = ent; + nentries++; + ptr += SizeOfXLUpgradeRelinkEntry; + } + appendStringInfo(buf, "%d files to relink; first rel %u/%u/%u fork %u seg %u", + nentries, + nentries ? first.tablespace_oid : 0, + nentries ? first.database_oid : 0, + nentries ? first.relfilenumber : 0, + nentries ? first.forknum : 0, + nentries ? first.segno : 0); + } + else if (info == XLOG_UPGRADE_RAWFILE) + { + xl_upgrade_rawfile xlrec; + char *path = rec + SizeOfXLUpgradeRawfile; + + memcpy(&xlrec, rec, SizeOfXLUpgradeRawfile); + appendStringInfo(buf, "rawfile \"%.*s\"; bytes %u", + (int) xlrec.path_len, path, xlrec.data_len); + } + else if (info == XLOG_UPGRADE_DIRTREE) + { + xl_upgrade_dirtree xlrec; + char *first = rec + SizeOfXLUpgradeDirtree; + + memcpy(&xlrec, rec, SizeOfXLUpgradeDirtree); + appendStringInfo(buf, "dirs %u (%u bytes); symlinks %u (%u bytes); first \"%s\"", + xlrec.ndirs, xlrec.dir_bytes, + xlrec.nsymlinks, xlrec.sym_bytes, + xlrec.ndirs > 0 ? first : ""); + } + else if (info == XLOG_UPGRADE_HANDOFF) + { + xl_pg_upgrade_handoff xlrec; + + memcpy(&xlrec, rec, SizeOfXLPgUpgradeHandoff); + appendStringInfo(buf, "old_major_version %u; target_major_version %u; time %lld", + xlrec.old_major_version, + xlrec.target_major_version, + (long long) xlrec.handoff_time); + } +} + +const char * +pg_upgrade_identify(uint8 info) +{ + switch (info & ~XLR_INFO_MASK) + { + case XLOG_UPGRADE_START: + return "PG_UPGRADE_START"; + case XLOG_UPGRADE_COMPLETE: + return "PG_UPGRADE_COMPLETE"; + case XLOG_UPGRADE_SLRU_DATA: + return "UPGRADE_SLRU_DATA"; + case XLOG_UPGRADE_RELFILE_DATA: + return "UPGRADE_RELFILE_DATA"; + case XLOG_UPGRADE_RELINK: + return "UPGRADE_RELINK"; + case XLOG_UPGRADE_RAWFILE: + return "UPGRADE_RAWFILE"; + case XLOG_UPGRADE_DIRTREE: + return "UPGRADE_DIRTREE"; + case XLOG_UPGRADE_HANDOFF: + return "PG_UPGRADE_HANDOFF"; + } + return NULL; +} diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c index 2468a7d257..190a22b564 100644 --- a/src/backend/access/rmgrdesc/xlogdesc.c +++ b/src/backend/access/rmgrdesc/xlogdesc.c @@ -218,6 +218,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record) { /* no further information to print */ } + /* Upgrade records are handled by pg_upgrade_desc() via RM_PG_UPGRADE_ID */ } const char * @@ -225,6 +226,16 @@ xlog_identify(uint8 info) { const char *id = NULL; + /* + * Upgrade record types must be checked before masking, since + * 0xC0/0xC1/0xC2 all reduce to 0xC0 after applying ~XLR_INFO_MASK (0xF0). + */ + + /* + * Upgrade records are handled by pg_upgrade_identify() via + * RM_PG_UPGRADE_ID + */ + switch (info & ~XLR_INFO_MASK) { case XLOG_CHECKPOINT_SHUTDOWN: diff --git a/src/backend/access/transam/Makefile b/src/backend/access/transam/Makefile index a32f473e0a..81294a8f96 100644 --- a/src/backend/access/transam/Makefile +++ b/src/backend/access/transam/Makefile @@ -37,7 +37,8 @@ OBJS = \ xlogrecovery.o \ xlogstats.o \ xlogutils.o \ - xlogwait.o + xlogwait.o \ + pgupgrade_wal.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c index 75012d4b8f..3eb6e4f5f0 100644 --- a/src/backend/access/transam/clog.c +++ b/src/backend/access/transam/clog.c @@ -1115,6 +1115,16 @@ clog_redo(XLogReaderState *record) elog(PANIC, "clog_redo: unknown op code %u", info); } +/* + * Restore a captured pg_xact (CLOG) segment during --wal-upgrade replay. + * Thin wrapper exposing XactCtl to the RM_PG_UPGRADE_ID redo handler. + */ +void +CLOGUpgradeRestoreSegment(int64 segno, const char *data, Size datalen) +{ + SlruUpgradeRestoreSegment(XactCtl, segno, data, datalen); +} + /* * Entrypoint for sync.c to sync clog files. */ diff --git a/src/backend/access/transam/meson.build b/src/backend/access/transam/meson.build index 06aadc7f31..cabc681622 100644 --- a/src/backend/access/transam/meson.build +++ b/src/backend/access/transam/meson.build @@ -6,6 +6,7 @@ backend_sources += files( 'generic_xlog.c', 'multixact.c', 'parallel.c', + 'pgupgrade_wal.c', 'rmgr.c', 'slru.c', 'subtrans.c', diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 8c69465813..7856ced105 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -2995,6 +2995,23 @@ multixact_redo(XLogReaderState *record) elog(PANIC, "multixact_redo: unknown op code %u", info); } +/* + * Restore captured pg_multixact/offsets and pg_multixact/members segments + * during --wal-upgrade replay. Thin wrappers exposing the static SLRU + * ctls to the RM_PG_UPGRADE_ID redo handler. + */ +void +MultiXactOffsetUpgradeRestoreSegment(int64 segno, const char *data, Size datalen) +{ + SlruUpgradeRestoreSegment(MultiXactOffsetCtl, segno, data, datalen); +} + +void +MultiXactMemberUpgradeRestoreSegment(int64 segno, const char *data, Size datalen) +{ + SlruUpgradeRestoreSegment(MultiXactMemberCtl, segno, data, datalen); +} + /* * Entrypoint for sync.c to sync offsets files. */ diff --git a/src/backend/access/transam/pgupgrade_wal.c b/src/backend/access/transam/pgupgrade_wal.c new file mode 100644 index 0000000000..14c20feed6 --- /dev/null +++ b/src/backend/access/transam/pgupgrade_wal.c @@ -0,0 +1,1963 @@ +/* + * pgupgrade_wal.c + * + * WAL redo and startup handling for RM_PG_UPGRADE_ID records written by + * pg_upgrade --wal-upgrade: + * + * XLOG_UPGRADE_START (0x00) -- window open, write PG_VERSION + * XLOG_UPGRADE_COMPLETE (0x10) -- window close, informational + * XLOG_UPGRADE_SLRU_DATA (0x20) -- bulk SLRU segment image + * XLOG_UPGRADE_RELFILE_DATA(0x30) -- bulk relation file segment image (system rels) + * XLOG_UPGRADE_DIRTREE (0x40) -- initdb directory + symlink skeleton + * XLOG_UPGRADE_RAWFILE (0x50) -- verbatim non-relation file image + * XLOG_UPGRADE_HANDOFF (0x60) -- stand-down trigger for an old-format standby + * XLOG_UPGRADE_RELINK (0x70) -- manifest of USER relation files a streaming + * standby links from its retained old datadir + * (the window omits their data) + * + * The XID/OID/multixact counters are not WAL-logged: they are transplanted into + * pg_control before the end-of-upgrade checkpoint, which carries them, and + * recovery reproduces them from that checkpoint. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/access/transam/pgupgrade_wal.c + */ +#include "postgres.h" + +#include +#include +#include + +#include "access/clog.h" /* CLOGUpgradeRestoreSegment */ +#include "access/multixact.h" /* MultiXact*UpgradeRestoreSegment */ +#include "access/pgupgrade_wal.h" +#include "access/slru.h" +#include "access/xlog.h" +#include "access/xlog_internal.h" +#include "access/xlogrecovery.h" /* pgUpgradeReplayInProgress */ +#include "access/xloginsert.h" +#include "access/xlogreader.h" +#include "access/xlogutils.h" /* wal_segment_close */ +#include "catalog/pg_control.h" +#include "catalog/pg_tablespace_d.h" +#include "common/controldata_utils.h" /* get_controlfile for local CN + * derivation */ +#include "common/file_perm.h" /* pg_dir_create_mode */ +#include "common/file_utils.h" /* pg_clone_file, pg_copy_file_range_all */ +#include "common/relpath.h" /* RelFileLocator, ForkNumber */ +#include "miscadmin.h" +#include "storage/bufmgr.h" /* buffer-manager RELFILE_DATA redo */ +#include "storage/smgr.h" /* smgr create for empty relfiles */ +#include "storage/bufpage.h" /* PageSetLSN */ +#include "storage/fd.h" +#include "storage/copydir.h" /* copydir() for WAL segment migration */ +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "replication/walreceiver.h" /* libpqwalreceiver client for + * auto-anchor */ +#include "utils/elog.h" + +/* ------------------------------------------------------------------------- + * pg_upgrade WAL-replay-based atomicity check + * ------------------------------------------------------------------------- + */ + +/* + * Private state for the XLogReader used by UpgradeWalScanMarkers(). + */ +typedef struct UpgradeWalReadPrivate +{ + char dir[MAXPGPATH]; /* WAL directory to read segments from */ + TimeLineID tli; /* timeline (always 1 for upgrade WAL) */ + XLogRecPtr endptr; /* one past the last byte of available WAL */ + int openerr_elevel; /* ereport level if a segment cannot be opened */ +} UpgradeWalReadPrivate; + +static void +UpgradeWalSegOpen(XLogReaderState *state, XLogSegNo nextSegNo, TimeLineID *tli_p) +{ + UpgradeWalReadPrivate *priv = (UpgradeWalReadPrivate *) state->private_data; + char fname[MAXFNAMELEN]; + char path[MAXPGPATH]; + + XLogFileName(fname, priv->tli, nextSegNo, state->segcxt.ws_segsize); + snprintf(path, sizeof(path), "%s/%s", priv->dir, fname); + state->seg.ws_file = BasicOpenFile(path, O_RDONLY | PG_BINARY); + if (state->seg.ws_file < 0) + ereport(priv->openerr_elevel, + (errcode_for_file_access(), + errmsg("could not open upgrade WAL segment \"%s\": %m", path))); +} + +static void +UpgradeWalSegClose(XLogReaderState *state) +{ + if (state->seg.ws_file >= 0) + close(state->seg.ws_file); + state->seg.ws_file = -1; +} + +static int +UpgradeWalPageRead(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, + XLogRecPtr targetRecPtr, char *readBuf) +{ + UpgradeWalReadPrivate *priv = (UpgradeWalReadPrivate *) state->private_data; + int count = XLOG_BLCKSZ; + WALReadError errinfo; + + /* Never read past the last available byte of WAL. */ + if (targetPagePtr + XLOG_BLCKSZ > priv->endptr) + { + if (targetPagePtr + reqLen > priv->endptr) + return -1; + count = (int) (priv->endptr - targetPagePtr); + } + + if (!WALRead(state, readBuf, targetPagePtr, count, priv->tli, &errinfo)) + return -1; + + return count; +} + +/* + * Parse the WAL in "waldir" and locate the pg_upgrade markers plus the + * end-of-upgrade checkpoint (CN) that recovery must anchor at. A real + * XLogReader is used, not a byte-pattern match: the upgrade WAL is full of + * arbitrary full-page-image bytes, so any fixed byte pair recurs by chance. + * + * Out-params: + * found_start / found_complete -- the START / COMPLETE markers were seen. + * cn -- CheckPoint of the last online checkpoint preceding START. This + * is CN, the recovery anchor; it carries the transplanted + * XID/OID/multixact counters. + * cn_lsn -- record LSN of that checkpoint (-> ControlFile.checkPoint). + * + * Returns false if there is no readable WAL at all. A present-but-unreadable or + * corrupt window (a segment cannot be opened, a checkpoint record is malformed) + * stops recovery with FATAL: this runs in the startup process, where a broken + * local window must not be silently ignored. + */ +bool +UpgradeWalScanMarkers(const char *waldir, bool *found_start, + bool *found_complete, CheckPoint *cn, + XLogRecPtr *cn_lsn, uint64 *wal_sysid) +{ + DIR *dir; + struct dirent *de; + int segsize = 0; + XLogSegNo lowseg = 0; + XLogSegNo highseg = 0; + XLogSegNo runstart = 0; + bool any = false; + char runstart_path[MAXPGPATH] = {0}; + UpgradeWalReadPrivate priv; + XLogReaderState *reader; + XLogRecPtr startptr; + XLogRecPtr first; + CheckPoint last_ckpt; + XLogRecPtr last_ckpt_lsn = InvalidXLogRecPtr; + + *found_start = false; + *found_complete = false; + *cn_lsn = InvalidXLogRecPtr; + *wal_sysid = 0; + MemSet(cn, 0, sizeof(CheckPoint)); + MemSet(&last_ckpt, 0, sizeof(CheckPoint)); + + /* + * First pass over the directory: determine the segment size (all WAL + * segment files are exactly one segment long) and the lowest/highest + * segment numbers present. + */ + dir = AllocateDir(waldir); + if (dir == NULL) + return false; + while ((de = ReadDir(dir, waldir)) != NULL) + { + TimeLineID ftli; + XLogSegNo segno; + char path[MAXPGPATH]; + struct stat st; + + if (!IsXLogFileName(de->d_name)) + continue; + + if (segsize == 0) + { + snprintf(path, sizeof(path), "%s/%s", waldir, de->d_name); + if (stat(path, &st) != 0 || st.st_size == 0) + continue; + segsize = (int) st.st_size; + } + + XLogFromFileName(de->d_name, &ftli, &segno, segsize); + + /* + * The upgrade WAL (CN..COMPLETE) is always on timeline 1. Only bound + * the scan by TLI-1 segments: after a standby's end-of-recovery + * timeline switch, higher-TLI segments also live in pg_wal/, and + * including them would push the scan's end past the last TLI-1 + * segment, so the reader would try to open a nonexistent 00000001... + * segment and FATAL. + */ + if (ftli != 1) + continue; + if (!any || segno < lowseg) + lowseg = segno; + if (!any || segno > highseg) + highseg = segno; + any = true; + } + FreeDir(dir); + + if (!any || segsize == 0) + return false; + + /* + * Bound the scan to the contiguous run of TLI-1 segments ending at + * highseg, not from lowseg. The upgrade window is always the topmost + * contiguous run; when delivered by archive-PITR staging, pg_wal/ can + * also hold unrelated pre-window segments from the restored base backup, + * with a gap between them. Starting at lowseg would make + * XLogFindNextRecord walk into that hole and FATAL. Walk down from + * highseg while each preceding segment is present. (On the primary's own + * first start the window is the only content, so runstart == lowseg.) + */ + runstart = highseg; + { + char segname[MAXFNAMELEN]; + char segpath[MAXPGPATH]; + struct stat st; + + while (runstart > lowseg) + { + XLogFileName(segname, 1, runstart - 1, segsize); + snprintf(segpath, sizeof(segpath), "%s/%s", waldir, segname); + if (stat(segpath, &st) != 0) + break; /* gap: runstart-1 is missing */ + runstart--; + } + XLogFileName(segname, 1, runstart, segsize); + snprintf(runstart_path, sizeof(runstart_path), "%s/%s", waldir, segname); + } + + /* + * Capture the system identifier the upgrade WAL was emitted under, from + * xlp_sysid in the run-start segment's long page header. Recovery + * validates every WAL page's xlp_sysid against + * pg_control->system_identifier, so the arming step stamps pg_control + * with this value -- letting a fresh skeleton adopt the sysid in-process + * from the WAL, exactly as it does CN, with no offline sysid stamping. + */ + { + int fd = OpenTransientFile(runstart_path, O_RDONLY | PG_BINARY); + XLogLongPageHeaderData longhdr; + + if (fd >= 0) + { + if (pg_pread(fd, &longhdr, sizeof(longhdr), 0) == sizeof(longhdr) && + longhdr.std.xlp_magic == XLOG_PAGE_MAGIC && + (longhdr.std.xlp_info & XLP_LONG_HEADER)) + *wal_sysid = longhdr.xlp_sysid; + CloseTransientFile(fd); + } + } + + priv.tli = 1; + priv.openerr_elevel = FATAL; + strlcpy(priv.dir, waldir, sizeof(priv.dir)); + XLogSegNoOffsetToRecPtr(runstart, 0, segsize, startptr); + XLogSegNoOffsetToRecPtr(highseg + 1, 0, segsize, priv.endptr); + + reader = XLogReaderAllocate(segsize, NULL, + XL_ROUTINE(.page_read = UpgradeWalPageRead, + .segment_open = UpgradeWalSegOpen, + .segment_close = UpgradeWalSegClose), + &priv); + if (reader == NULL) + return false; + + /* + * Find the first valid record at/after the start of the run-start + * segment. + */ + { + char *errormsg = NULL; + + first = XLogFindNextRecord(reader, startptr, &errormsg); + } + if (XLogRecPtrIsInvalid(first)) + { + XLogReaderFree(reader); + return false; + } + + XLogBeginRead(reader, first); + for (;;) + { + char *errormsg; + XLogRecord *record = XLogReadRecord(reader, &errormsg); + uint8 rmid; + uint8 info; + + if (record == NULL) + break; /* end of WAL or unreadable -- stop */ + + rmid = XLogRecGetRmid(reader); + info = XLogRecGetInfo(reader) & ~XLR_INFO_MASK; + + if (rmid == RM_XLOG_ID && + (info == XLOG_CHECKPOINT_ONLINE || info == XLOG_CHECKPOINT_SHUTDOWN)) + { + /* + * Track the most recent checkpoint so that, on reaching START, we + * capture CN (the last checkpoint preceding START). "Already + * applied?" is decided by the caller from the control file, not + * by detecting a post-COMPLETE checkpoint here (it may be on a + * later timeline this TLI-1 scan cannot read). + */ + if (XLogRecGetDataLen(reader) < sizeof(CheckPoint)) + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("checkpoint record too short"))); + memcpy(&last_ckpt, XLogRecGetData(reader), sizeof(CheckPoint)); + last_ckpt_lsn = reader->ReadRecPtr; + } + else if (rmid == RM_PG_UPGRADE_ID) + { + if (info == XLOG_UPGRADE_START) + { + *found_start = true; + /* CN is the checkpoint immediately preceding START */ + *cn = last_ckpt; + *cn_lsn = last_ckpt_lsn; + } + else if (info == XLOG_UPGRADE_COMPLETE) + { + *found_complete = true; + break; /* window is closed; nothing after COMPLETE + * matters */ + } + } + } + + XLogReaderFree(reader); + return true; +} + + +/* + * True once PerformWalUpgradeIfNeeded() has armed the sanctioned upgrade + * bootstrap for this startup. The redo handlers consult it to distinguish the + * bootstrap replay (apply the upgrade images) from an ordinary/standby stream + * that merely contains these records (stop and require a restart). + * Startup-process-local. + */ +static bool in_upgrade_bootstrap = false; + +/* + * Set when first-startup armed the STREAMING-STANDBY path (a fresh skeleton + * auto-armed from the primary), as opposed to the primary's own crash recovery + * or an archive/PITR restore. On the streaming path the skeleton is empty of + * user data, so an XLOG_UPGRADE_RELINK manifest with no old-datadir path to link + * from is a fatal misconfiguration (the user relations would silently be + * absent); on the other two paths the files are already on disk, so the same + * manifest is a legitimate no-op. Startup-process-local. + */ +static bool armed_streaming_standby = false; + +/* + * The CN LSN the streaming-standby path armed recovery at (locally derived from + * the retained old datadir; see ArmFromLocalDerivationIfConfigured). Recorded so + * the XLOG_UPGRADE_START redo handler can verify the checkpoint recovery actually + * began at matches the derivation -- turning a wrong derivation into a clear FATAL + * rather than silent mis-recovery. InvalidXLogRecPtr on every other path. + * Startup-process-local. + */ +static XLogRecPtr armed_cn_lsn = InvalidXLogRecPtr; + +/* + * Is the durable "window reached COMPLETE" flag set in pg_control? It is set + * and fsync'd by the XLOG_UPGRADE_COMPLETE redo handler (and by pg_upgrade on + * the primary, which does not replay) the instant the window reaches COMPLETE, + * so it distinguishes a completed upgrade from a crashed partial one even when + * a torn final WAL page hides the COMPLETE record from the scan. The caller + * pairs it with a control checkpoint past CN to decide "finalized" (see + * PerformWalUpgradeIfNeeded). Reads the control file already loaded by + * LocalProcessControlFile() before the startup process runs. + */ +static bool +UpgradeWindowFinalized(void) +{ + return GetControlFileUpgradeFinalized(); +} + + +/* + * Compute the CN segment number by replicating pg_resetwal's FindEndOfXLOG rule + * against the retained old datadir, matching the segment the producer's + * "pg_resetwal -l" targeted on the primary: + * + * CN_seg = max( XLByteToSeg(old_tail, old_seg_size), + * highest segment in /pg_wal ) + 1 + * + * The scan takes the max segno over all timelines and over partial (.partial) + * segments, without a timeline filter, exactly as pg_resetwal.c FindEndOfXLOG + * does. This must match pg_resetwal because the producer's -l target came from + * the old cluster's own "pg_resetwal -n" result: if the old cluster was ever + * promoted, a higher-timeline segment could hold the max segno, and a + * TLI-1-only scan would compute a smaller CN_seg than the producer targeted. + * (The window itself is on timeline 1, but this is a floor computation, not a + * window scan.) The +1 advances into virgin territory, as pg_resetwal does. + * Segment math uses the old cluster's segment size. + */ +static XLogSegNo +DeriveUpgradeCnSegment(const char *old_datadir, XLogRecPtr old_tail, + int old_seg_size) +{ + char waldir[MAXPGPATH]; + DIR *dir; + struct dirent *de; + XLogSegNo maxseg; + + /* Floor: the segment holding the old cluster's checkpoint redo. */ + XLByteToSeg(old_tail, maxseg, old_seg_size); + + snprintf(waldir, sizeof(waldir), "%s/%s", old_datadir, XLOGDIR); + dir = AllocateDir(waldir); + if (dir == NULL) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not open old cluster WAL directory \"%s\": %m", + waldir))); + while ((de = ReadDir(dir, waldir)) != NULL) + { + TimeLineID ftli; + XLogSegNo segno; + + /* complete and partial segments both count (see header) */ + if (!IsXLogFileName(de->d_name) && !IsPartialXLogFileName(de->d_name)) + continue; + + XLogFromFileName(de->d_name, &ftli, &segno, old_seg_size); + (void) ftli; /* parsed for the API; not a filter */ + + if (segno > maxseg) + maxseg = segno; + } + FreeDir(dir); + + /* Advance by one into virgin territory (matches pg_resetwal). */ + return maxseg + 1; +} + +/* + * Is this data directory staged for --wal-upgrade recovery? True iff the + * pg_upgrade.signal sentinel is present. The sentinel is the single gate all + * three detection sites share (checkDataDir's control-file synthesis, and the + * streaming/archive arms in this file), so they classify a directory + * identically; the surrounding config (primary_conninfo vs recovery.signal) + * then selects streaming vs archive-PITR. + */ +bool +UpgradeSignalStaged(void) +{ + char path[MAXPGPATH]; + struct stat st; + + snprintf(path, sizeof(path), "%s/%s", DataDir, PG_UPGRADE_SIGNAL_FILE); + return stat(path, &st) == 0; +} + +/* + * Automatic streaming-standby arming, deriving CN LOCALLY. + * + * A fresh vN+1 skeleton with primary_conninfo set arms its control file at CN + * without asking the primary for the anchor: it derives CN itself from its own + * retained old data directory, exactly reproducing where the producer's + * pg_resetwal placed CN. The one thing it still needs from the primary is the + * system identifier -- the new cluster's sysid, which the window WAL pages are + * stamped with -- and that comes from the standard IDENTIFY_SYSTEM command it + * would run anyway. Runs in the startup process before StartupXLOG, so no SQL + * backend is needed. + * + * Derivation: + * 1. sysid from IDENTIFY_SYSTEM on the primary (== the new cluster's sysid). + * 2. old_tail = old cluster's checkPointCopy.redo, from its control file. + * 3. CN_seg = FindEndOfXLOG rule over the old datadir (DeriveUpgradeCnSegment). + * 4. CN is the DB_SHUTDOWNED checkpoint at the start of CN_seg on timeline 1: + * cn_lsn = segment-boundary LSN just past the long page header, and because + * CN is a shutdown checkpoint, redo == cn_lsn. + * + * Gated on the pg_upgrade.signal sentinel: only a skeleton staged for + * --wal-upgrade recovery carries it, so an ordinary streaming standby + * (primary_conninfo set, but no sentinel) is left entirely untouched and starts + * normally. The streaming mode is inferred from primary_conninfo being set (vs. + * the recovery.signal-driven archive path). Returns false (caller falls back to + * the local-window path) when the sentinel is absent or no primary is configured. + * A missing old datadir GUC, a connection failure, or an unreadable old control + * file while armed is a hard FATAL. + */ +static bool +ArmFromLocalDerivationIfConfigured(void) +{ + WalReceiverConn *conn; + char *err = NULL; + TimeLineID ignored_tli = 0; + char *sysid_str; + uint64 sysid = 0; + const char *old_datadir; + ControlFileData *old_control; + bool crc_ok = false; + XLogRecPtr old_tail; + int old_seg_size; + XLogSegNo cn_seg; + XLogRecPtr cn_lsn; + CheckPoint cn; + struct stat st; + + /* + * Only act on a skeleton explicitly staged for --wal-upgrade recovery + * (the pg_upgrade.signal sentinel). Without it an ordinary streaming + * standby would try to derive an upgrade anchor on every start. + */ + if (!UpgradeSignalStaged()) + return false; + + /* + * Once the window has been replayed to COMPLETE this node is a finalized + * new-version cluster and the durable pg_control upgrade_finalized flag + * is set (the COMPLETE redo fsync'd it). Re-deriving CN and re-arming at + * the old CN would rewind recovery behind this standby's actual replay + * position, and fail once the primary drops the retention slot, so a + * restart with the sentinel still staged falls through here and starts as + * an ordinary hot standby. This mirrors the local-window path's + * UpgradeWindowFinalized() guard. + */ + if (UpgradeWindowFinalized()) + return false; + + /* + * primary_conninfo set -> this is the streaming path (as opposed to the + * archive/PITR path, which has recovery.signal and no primary). + */ + if (PrimaryConnInfo == NULL || PrimaryConnInfo[0] == '\0') + return false; + + /* + * A streamed upgrade standby must also be in standby mode: arming the + * control file at CN commits this node to following the primary's forward + * WAL (which exists only on the primary). Without standby.signal the + * node would leave recovery and come up read-write at CN -- split-brain + * against the real primary. standby.signal is the operator's + * responsibility (like any standby); if the upgrade sentinel is staged + * with primary_conninfo but without it, refuse to arm rather than risk + * that outcome. + */ + { + char standbysig[MAXPGPATH]; + + snprintf(standbysig, sizeof(standbysig), "%s/standby.signal", DataDir); + if (stat(standbysig, &st) != 0) + ereport(FATAL, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("pg_upgrade streaming skeleton has \"%s\" and primary_conninfo but no \"standby.signal\"", + PG_UPGRADE_SIGNAL_FILE), + errhint("Create a standby.signal file so this node follows the primary; a streamed upgrade target must run in standby mode."))); + } + + /* + * The old datadir path is standby-local (the primary cannot know it); the + * operator supplies it in the skeleton's postgresql.conf. Without it CN + * cannot be derived, so this is a hard misconfiguration. + */ + old_datadir = pg_upgrade_standby_old_datadir; + if (old_datadir == NULL || old_datadir[0] == '\0') + ereport(FATAL, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("streaming --wal-upgrade skeleton requires \"pg_upgrade_standby_old_datadir\" to derive the upgrade anchor"), + errhint("Set \"pg_upgrade_standby_old_datadir\" in postgresql.conf to this standby's retained pre-upgrade data directory."))); + + /* + * Obtain the sysid from IDENTIFY_SYSTEM on the primary. This is the + * primary's == the new cluster's sysid == what the window WAL pages are + * stamped with, so recovery's per-page xlp_sysid check passes once the + * control file adopts it. The returned TLI is ignored: CN lives on + * timeline 1 (hardcoded below). A connection failure while the sentinel + * is staged is a hard FATAL. + */ + load_file("libpqwalreceiver", false); + if (WalReceiverFunctions == NULL) + elog(FATAL, "libpqwalreceiver didn't initialize correctly"); + + conn = walrcv_connect(PrimaryConnInfo, true, false, false, + "pg_upgrade_anchor", &err); + if (conn == NULL) + ereport(FATAL, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("could not connect to the primary to identify the pg_upgrade system: %s", + err ? err : "unknown error"), + errhint("Set primary_conninfo to a live --wal-upgrade primary."))); + + sysid_str = walrcv_identify_system(conn, &ignored_tli); + walrcv_disconnect(conn); + if (sysid_str == NULL || + sscanf(sysid_str, "%" SCNu64, &sysid) != 1 || + sysid == 0) + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("malformed system identifier from primary: \"%s\"", + sysid_str ? sysid_str : "(null)"))); + + /* + * Read the retained old cluster's control file to get old_tail (its clean + * shutdown checkpoint redo == checkPoint) and its WAL segment size. + * Using the backend get_controlfile(): it OpenTransientFile()s the file, + * palloc's a copy, and CRC-checks it -- no shared state, safe from the + * startup process before StartupXLOG. + * + * Pre-check that the control file is present and readable: + * get_controlfile() ereport(ERROR)s with a generic "could not open file" + * if it is missing or unreadable, which the startup process turns into a + * bare postmaster restart loop with no hint at the real cause. Fail with + * a clear, actionable FATAL instead (a misconfigured + * pg_upgrade_standby_old_datadir is the usual cause). + */ + { + char ctlpath[MAXPGPATH]; + struct stat st; + + snprintf(ctlpath, sizeof(ctlpath), "%s/global/pg_control", old_datadir); + if (stat(ctlpath, &st) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not access old cluster control file \"%s\": %m", + ctlpath), + errhint("\"pg_upgrade_standby_old_datadir\" must point at this standby's retained pre-upgrade data directory."))); + } + + old_control = get_controlfile(old_datadir, &crc_ok); + if (!crc_ok) + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("pg_control CRC check failed for old data directory \"%s\"", + old_datadir), + errhint("The retained pre-upgrade data directory named by \"pg_upgrade_standby_old_datadir\" is unreadable or corrupt."))); + + old_tail = old_control->checkPointCopy.redo; + old_seg_size = (int) old_control->xlog_seg_size; + pfree(old_control); + + /* + * Byte-contiguous CN derivation assumes the new cluster's WAL segment + * size equals the old cluster's: pg_upgrade --wal-upgrade-exact positions + * the new WAL at the old cluster's next SEGMENT, and we recompute that + * segment here from the old datadir using old_seg_size. If the two + * differ (the upgrade changed wal_segment_size), the segment-to-LSN + * mapping on each side no longer lines up and the derived CN would be + * wrong. Recovery's START-time guard would eventually catch the mismatch + * and FATAL, but far downstream and opaquely; reject it here with a clear + * message instead. (wal_segment_size is this node's, already loaded from + * its control file by LocalProcessControlFile() before StartupXLOG.) + */ + if (old_seg_size != wal_segment_size) + ereport(FATAL, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("pg_upgrade streaming standby requires matching WAL segment sizes"), + errdetail("The retained old data directory uses %d-byte WAL segments but this cluster uses %d-byte segments.", + old_seg_size, wal_segment_size), + errhint("Re-initialize the new cluster with the same --wal-segsize as the old cluster before streaming the upgrade window."))); + + /* Replicate pg_resetwal's FindEndOfXLOG rule over the old datadir. */ + cn_seg = DeriveUpgradeCnSegment(old_datadir, old_tail, old_seg_size); + + /* + * CN is the DB_SHUTDOWNED checkpoint the producer wrote at the very start + * of CN_seg on timeline 1: the record begins just past the segment's long + * page header. Because it is a shutdown checkpoint, redo == checkPoint + * == that segment-boundary LSN. + */ + XLogSegNoOffsetToRecPtr(cn_seg, SizeOfXLogLongPHD, old_seg_size, cn_lsn); + + MemSet(&cn, 0, sizeof(cn)); + cn.redo = cn_lsn; + cn.ThisTimeLineID = 1; + cn.PrevTimeLineID = 1; + + /* + * The remaining CheckPoint counters (nextXid/nextOid/... ) are left zero: + * ArmControlFileForUpgradeRecovery() copies *cn into checkPointCopy only + * to seed recovery's start point, and StartupXLOG re-reads them from the + * real CN checkpoint record the moment it is streamed in and replayed. + * So the zero counters here are transient placeholders, never consulted + * for anything durable. + */ + armed_cn_lsn = cn_lsn; + + ereport(LOG, + (errmsg("auto-armed streaming standby from locally derived anchor " + "(sysid " UINT64_FORMAT ", CN %X/%08X, redo %X/%08X, TLI 1, CN segment %llu)", + sysid, LSN_FORMAT_ARGS(cn_lsn), + LSN_FORMAT_ARGS(cn.redo), (unsigned long long) cn_seg))); + + ArmControlFileForUpgradeRecovery(&cn, cn_lsn, sysid, true); + return true; +} + +/* + * Detect a pending pg_upgrade --wal-upgrade and, if found, arm recovery to + * replay it. Called from the startup process before StartupXLOG(). Dispatches + * to one of three paths: + * + * - streaming standby: a fresh skeleton with primary_conninfo derives CN + * locally and arms to stream the window from the primary; + * - local window: START/COMPLETE markers already in pg_wal/ are scanned, + * CN derived, and the control file armed to replay them; + * - archive PITR: no local window, but the sentinel is staged, so the + * bootstrap is armed for the window arriving via restore_command. + * + * Returns true when it armed the bootstrap (caller proceeds into StartupXLOG), + * false for an ordinary (non-upgrade or already-finalized) start. + */ +bool +PerformWalUpgradeIfNeeded(void) +{ + char wal_dir[MAXPGPATH]; + bool found_start = false; + bool found_complete = false; + CheckPoint cn; + XLogRecPtr cn_lsn = InvalidXLogRecPtr; + uint64 wal_sysid = 0; + + /* Skip during pg_upgrade internal server starts (-b binary upgrade mode) */ + if (IsBinaryUpgrade) + return false; + + /* + * STREAMING STANDBY PATH. A fresh skeleton with primary_conninfo set + * derives CN locally from its retained old data directory and arms the + * control file (sysid + CN + TLI=1), then lets StartupXLOG() enter + * standby mode and stream the window forward. Returns false when no + * primary is configured (fall through to the local-window path). + */ + if (ArmFromLocalDerivationIfConfigured()) + { + in_upgrade_bootstrap = true; + armed_streaming_standby = true; + + /* + * Suppress hot standby from the very start of recovery, before any + * record replays. A streaming standby has no shared catalogs (those + * under global/) on disk until the window streams in; without this, + * recovery could reach consistency and admit a read-only connection + * in the gap before XLOG_UPGRADE_START, which would FATAL opening a + * not-yet-materialized catalog. Held until XLOG_UPGRADE_COMPLETE + * clears it. + */ + pgUpgradeReplayInProgress = true; + + return true; + } + + snprintf(wal_dir, sizeof(wal_dir), XLOGDIR); + + /* + * LOCAL-WINDOW PATH. Scan pg_wal/ for the START/COMPLETE markers and CN. + * A completed --wal-upgrade run leaves a START..COMPLETE window in + * pg_wal/ (no rename). Cases: + * + * pending (not finalized) -> derive CN from the WAL, arm pg_control + * in-process, and let StartupXLOG() recover the window. already applied + * (COMPLETE marker present, or control checkpoint > CN) -> normal + * startup; a prior startup finalized the upgrade. START, no COMPLETE and + * not finalized -> crash mid-upgrade; FATAL (see below). no START -> not + * an upgrade; normal startup. + * + * Deriving CN here (rather than a prior offline pg_resetwal stamp) lets + * the same WAL stream drive recovery on the primary and on a physical + * standby. + */ + if (!UpgradeWalScanMarkers(wal_dir, &found_start, &found_complete, + &cn, &cn_lsn, &wal_sysid)) + { + /* + * ARCHIVE-PITR PATH. No local window, but a cross-version + * upgrade-PITR restore stages the pg_upgrade.signal sentinel (the + * same marker checkDataDir() keys the control-file synthesis on): the + * window arrives later via restore_command as recovery replays + * forward from a pre-upgrade base backup across the upgrade boundary. + * Recovery starts at the base backup's checkpoint and flows through + * CN organically, so no re-anchoring is needed here; just arm + * in_upgrade_bootstrap so the XLOG_UPGRADE_START redo does not FATAL + * when the window is reached. (This is the archive path because there + * is no local window and, on this branch, recovery.signal drives the + * restore rather than primary_conninfo.) + * + * Gate on the sentinel (not raw recovery.signal/standby.signal): an + * ordinary archive PITR or a plain streaming standby must not arm the + * bootstrap, or the standby-safety FATAL-halt guard is defeated. This + * keeps all three detection sites (here, checkDataDir, and the + * streaming path) on the one pg_upgrade.signal sentinel. + */ + if (UpgradeSignalStaged()) + { + ereport(LOG, + (errmsg("archive recovery active with no local pg_upgrade window; " + "arming upgrade replay in case the window arrives from the archive"))); + in_upgrade_bootstrap = true; + + /* + * Suppress hot standby from the very start of recovery, as the + * local-window and streaming paths do. On this path recovery + * replays a pre-upgrade base backup forward and can reach + * consistency well before the window's XLOG_UPGRADE_START, so a + * read-only backend could otherwise be admitted against the still + * old-version, half-upgraded catalog. Hot-standby activation is + * a one-way latch, so the flag must be set here at arm time, not + * later in the START redo handler (by then a backend may already + * be in). Cleared at XLOG_UPGRADE_COMPLETE. + */ + pgUpgradeReplayInProgress = true; + } + return false; /* let StartupXLOG drive recovery from + * backup_label */ + } + + /* + * Crash-atomicity guard, WAL-scan-independent. The burst server durably + * set upgrade_started in pg_control just before emitting + * XLOG_UPGRADE_START; the COMPLETE path sets upgrade_finalized. So + * upgrade_started && !finalized is a partial (crashed) upgrade that must + * never auto-serve its half-built catalog -- even if the START-bearing + * WAL did not survive to first boot (byte- contiguous CN generation can + * recycle those segments, so the shutdown checkpoint then hides START + * from the scan above and found_start is false). Refuse before the + * found_start early-return that would otherwise treat this as an ordinary + * cluster. The old cluster was never written, so re-running pg_upgrade + * is the safe recovery. + */ + if (GetControlFileUpgradeStarted() && !UpgradeWindowFinalized()) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_upgrade WAL is incomplete: found START without COMPLETE"), + errhint("The upgrade did not finish; re-run pg_upgrade from the old cluster (which is intact)."))); + + if (!found_start) + return false; /* not an upgrade */ + + /* + * Already finalized? Requires both durable signals: the pg_control + * upgrade_finalized flag (COMPLETE was reached) and the control + * checkpoint strictly past CN (a checkpoint was written after the + * window). The flag alone is insufficient because the COMPLETE handler + * sets it one checkpoint before the control checkpoint advances, so a + * crash in that gap must be re-armed and re-replayed (idempotent), not + * skipped. A checkpoint past CN alone is insufficient because a + * smart-shut-down partial upgrade also has one but never set the flag, + * and must be refused. Checking this before the partial-window diagnosis + * also means a finalized cluster whose final WAL page is torn (COMPLETE + * missed by the scan) is still recognized as done. + */ + if (UpgradeWindowFinalized() && + !XLogRecPtrIsInvalid(cn_lsn) && GetControlFileCheckPointLSN() > cn_lsn) + return false; /* finalized; ordinary startup */ + + /* + * A complete window whose checkpoint has not yet advanced past CN is a + * pending or mid-finalization upgrade (first start, or a crash after the + * COMPLETE marker but before the end-of-recovery checkpoint). Fall + * through to arm and (re-)replay it -- the window images are idempotent. + * Only a window that never reached COMPLETE is a genuine partial upgrade: + * the catalog is half-built and, since the new cluster auto-serves + * read-write at end of recovery, arming it would serve a corrupt catalog. + * Refuse; the old cluster was never written and is intact, so re-running + * pg_upgrade is the safe recovery. + */ + if (!found_complete) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_upgrade WAL is incomplete: found START without COMPLETE"), + errhint("The upgrade did not finish; re-run pg_upgrade from the old cluster (which is intact)."))); + + /* + * Pending. CN must have been found; otherwise the WAL is malformed and + * re-arming at an invalid LSN would corrupt recovery. + */ + if (XLogRecPtrIsInvalid(cn_lsn)) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_upgrade WAL is missing the end-of-upgrade checkpoint"), + errhint("Re-run pg_upgrade from the old cluster to start fresh."))); + + ereport(LOG, + (errmsg("pg_upgrade WAL found in pg_wal/; arming recovery from end-of-upgrade checkpoint at %X/%08X", + LSN_FORMAT_ARGS(cn_lsn)))); + + /* + * Arm the control file in-process: point recovery at CN (state = + * DB_IN_PRODUCTION, wal_level = replica) and adopt the upgrade WAL's + * system identifier so recovery's per-page xlp_sysid check passes. + * StartupXLOG() (called right after) reads ControlFile->checkPointCopy. + * Deriving CN and the sysid from the WAL here lets the same WAL stream + * drive recovery on the primary and on a physical standby, with no + * offline stamping step. + */ + ArmControlFileForUpgradeRecovery(&cn, cn_lsn, wal_sysid, false); + + /* + * Arm the sanctioned bootstrap so the redo handlers may apply the upgrade + * images. A pg_upgrade record reached without this flag came in through + * an ordinary/standby stream and must not be applied live (see + * pg_upgrade_redo). + */ + in_upgrade_bootstrap = true; + + /* + * Suppress hot standby before any record replays, as the streaming path + * does: the window's full-page images rebuild the catalogs as they + * replay, so a read-only connection admitted between consistency and + * XLOG_UPGRADE_START could observe a half-built catalog. Held until + * XLOG_UPGRADE_COMPLETE clears it. Matters for archive-PITR (consistency + * can be reached well before CN); harmless for the primary's own restart. + */ + pgUpgradeReplayInProgress = true; + + return true; +} + +/* + * Place one user relation file from the standby's retained old datadir into the + * new skeleton during XLOG_UPGRADE_RELINK redo, reproducing the result + * pg_upgrade's transfer step produced on the primary for the given mode: + * + * COPY - independent full byte copy (copy_file()). + * CLONE - reflink/COW clone: copyfile(COPYFILE_CLONE_FORCE) on macOS, + * ioctl(FICLONE) on Linux (same primitives as cloneFile()). + * COPY_FILE_RANGE - copy_file_range(). + * LINK - per-file hardlink, sharing the old inode. + * SWAP - rename(), moving the file out of the old datadir. + * + * Like the reflink modes in pg_upgrade itself, CLONE/COPY_FILE_RANGE FATAL if the + * filesystem cannot reflink rather than fall back to a full copy -- so the standby + * either reproduces the operator's chosen space profile or refuses, never silently + * costs 2x. The caller has already unlink()ed any pre-existing dst. + * + * The placed file is fsync'd before returning (the caller fsyncs the parent dir). + * These files bypass smgr, so the checkpointer has no sync request for them and the + * end-of-recovery checkpoint would otherwise advance pg_control past CN with the + * data only in the OS cache -- a crash there would leave a finalized upgrade with + * missing user relations, never re-replayed. + */ +static void +RelinkPlaceFile(const char *oldfile, const char *newfile, uint8 mode) +{ + switch (mode) + { + case UPGRADE_RELINK_MODE_LINK: + /* mirror --link: a per-file hardlink, sharing the old inode */ + if (link(oldfile, newfile) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not link \"%s\" to \"%s\": %m", + oldfile, newfile))); + break; + + case UPGRADE_RELINK_MODE_SWAP: + + /* + * Mirror --swap: move the file out of the old datadir (upstream + * swap renames whole DB dirs, consuming the old cluster). Unlike + * --link this leaves no entry behind. rename() over an existing + * dst is atomic and idempotent on replay (a re-run finds the + * source gone via the caller's stat() and skips). + */ + if (rename(oldfile, newfile) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not move \"%s\" to \"%s\": %m", + oldfile, newfile))); + break; + + case UPGRADE_RELINK_MODE_CLONE: + { + int save_errno; + + /* + * Reproduce pg_upgrade's cloneFile() via the shared helper, + * so the primary's transfer and this standby-side placement + * use one implementation (copyfile(COPYFILE_CLONE_FORCE) on + * macOS, else ioctl(FICLONE) on Linux). + */ + switch (pg_clone_file(oldfile, newfile, &save_errno)) + { + case PG_REFLINK_OK: + break; + case PG_REFLINK_ERROR: + errno = save_errno; + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not clone \"%s\" to \"%s\": %m", + oldfile, newfile), + errhint("The old data directory and the new skeleton must be on the same reflink-capable filesystem."))); + break; + case PG_REFLINK_UNSUPPORTED: + ereport(PANIC, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("file cloning not supported on this platform"), + errhint("The primary used --clone; a standby on this platform cannot reproduce it."))); + break; + } + break; + } + + case UPGRADE_RELINK_MODE_COPY_FILE_RANGE: + { + int save_errno; + + /* + * reproduce pg_upgrade's copyFileByRange() via the shared + * helper + */ + switch (pg_copy_file_range_all(oldfile, newfile, &save_errno)) + { + case PG_REFLINK_OK: + break; + case PG_REFLINK_ERROR: + errno = save_errno; + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not copy_file_range \"%s\" to \"%s\": %m", + oldfile, newfile))); + break; + case PG_REFLINK_UNSUPPORTED: + ereport(PANIC, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("copy_file_range not supported on this platform"), + errhint("The primary used --copy-file-range; a standby on this platform cannot reproduce it."))); + break; + } + break; + } + + case UPGRADE_RELINK_MODE_COPY: + default: + /* plain full copy: matches --copy (independent file, 2x space) */ + copy_file(oldfile, newfile); + break; + } + + /* + * Make the placed file's contents durable. copy_file() flushes but does + * not fsync (its bin/ callers fsync separately); the reflink/hardlink + * paths fsync nothing. Without this the end-of-recovery checkpoint could + * advance the control file past CN with the data still in the OS cache -- + * see the function header. The caller fsyncs the parent directory so the + * new dentry itself is durable. + */ + fsync_fname(newfile, false); +} + +/* + * Build the source path of a user relation in the retained old datadir for + * XLOG_UPGRADE_RELINK redo. + * + * For base/ and global/ relations the relpath is version-independent and we + * just prefix the old datadir. For a relation in a user-created tablespace the + * relpath GetRelationPath() produced embeds this (new) binary's + * TABLESPACE_VERSION_DIRECTORY -- "PG__" -- but the retained + * old datadir's tablespace area only holds the OLD version's directory + * ("PG__"). Blindly prefixing would name a nonexistent path + * and the entry would be silently skipped, losing every tablespace relation on + * the standby. So for the tablespace case we substitute the actual PG_* + * directory found under old_datadir/pg_tblspc//. Returns false if that + * directory cannot be resolved (no PG_* entry), so the caller can error rather + * than skip. + */ +static bool +RelinkBuildOldFile(const char *old_datadir, Oid spcOid, + const char *relpath, const char *segsuffix, + char *oldfile, size_t oldfile_size) +{ + const char *tblspc_prefix = PG_TBLSPC_DIR_SLASH; + size_t tblspc_prefix_len = strlen(tblspc_prefix); + const char *verseg; + const char *after_ver; + DIR *dir; + struct dirent *de; + char spcdir[MAXPGPATH]; + char oldverdir[MAXFNAMELEN]; + bool found = false; + + /* base/ and global/ are unversioned: a plain prefix is correct. */ + if (strncmp(relpath, tblspc_prefix, tblspc_prefix_len) != 0) + { + snprintf(oldfile, oldfile_size, "%s/%s%s", + old_datadir, relpath, segsuffix); + return true; + } + + /* + * Tablespace relpath: "pg_tblspc//PG_//". Locate the + * "PG_" segment (the component after "pg_tblspc//") and the + * tail that follows it ("//"). + */ + verseg = strchr(relpath + tblspc_prefix_len, '/'); + if (verseg == NULL) + return false; /* malformed; no version segment */ + verseg++; /* now at "PG_/..." */ + after_ver = strchr(verseg, '/'); + if (after_ver == NULL) + return false; /* malformed; no db/rel tail */ + + /* Find the single PG_* version directory under the old tablespace. */ + snprintf(spcdir, sizeof(spcdir), "%s/%s%u", + old_datadir, tblspc_prefix, spcOid); + dir = AllocateDir(spcdir); + if (dir == NULL) + return false; + while ((de = ReadDir(dir, spcdir)) != NULL) + { + if (strncmp(de->d_name, "PG_", 3) == 0) + { + strlcpy(oldverdir, de->d_name, sizeof(oldverdir)); + found = true; + break; + } + } + FreeDir(dir); + if (!found) + return false; + + /* Reassemble with the old version directory in place of the new one. */ + snprintf(oldfile, oldfile_size, "%s/%s%u/%s%s%s", + old_datadir, tblspc_prefix, spcOid, oldverdir, after_ver, + segsuffix); + return true; +} + + +/* ------------------------------------------------------------------------- + * Redo + * ------------------------------------------------------------------------- + */ + +void +pg_upgrade_redo(XLogReaderState *record) +{ + uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; + + if (info == XLOG_UPGRADE_START) + { + xl_pg_upgrade *xlrec = (xl_pg_upgrade *) XLogRecGetData(record); + int fd; + + /* + * pg_version is a fixed char[8]; a corrupt record may not + * NUL-terminate + */ + int len = strnlen(xlrec->pg_version, sizeof(xlrec->pg_version)); + + /* + * Standby / ordinary-stream guard. The upgrade image records carry + * the old cluster's page LSNs and are only safe to apply from the + * sanctioned bootstrap (anchored at CN into a non-serving data + * directory). Reaching START without in_upgrade_bootstrap means an + * ordinary/standby stream, so FATAL at the boundary rather than apply + * the window live. + * + * For a physical standby this FATAL is the intentional halt: the + * operator installs the new-version binary and relaunches, and + * startup then anchors at CN and replays the window. StandbyMode + * selects the message so the operator sees which case fired. + */ + if (!in_upgrade_bootstrap) + { + if (StandbyMode) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("reached pg_upgrade boundary on standby; halting to apply the upgrade"), + errdetail("A --wal-upgrade was performed on the primary; the standby cannot apply it while streaming."), + errhint("Install the new-version binaries and restart this standby; it will replay the upgrade from the end-of-upgrade checkpoint."))); + else + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_upgrade WAL encountered during replay"), + errhint("Restart this server to apply the pg_upgrade; " + "the upgrade cannot be replayed on a running standby."))); + } + + /* + * Streaming-standby derivation guard. On the streaming path CN is + * derived locally (ArmFromLocalDerivationIfConfigured) and recovery + * armed at armed_cn_lsn. If that derivation put CN at the wrong + * segment, recovery would anchor at the wrong LSN and silently + * mis-recover. CN is the shutdown checkpoint immediately preceding + * this START, so recovery's redo pointer here must still equal the + * LSN we armed at. A mismatch means the derived CN did not match the + * streamed window: FATAL clearly rather than continue. (On the + * local-window and archive paths armed_cn_lsn is Invalid and this + * check is skipped; there CN comes from the WAL scan itself, not a + * derivation that could disagree.) + */ + if (!XLogRecPtrIsInvalid(armed_cn_lsn)) + { + XLogRecPtr redo_now = GetRedoRecPtr(); + + if (redo_now != armed_cn_lsn) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("derived pg_upgrade CN (%X/%08X) did not match the streamed window (recovery redo at %X/%08X)", + LSN_FORMAT_ARGS(armed_cn_lsn), + LSN_FORMAT_ARGS(redo_now)), + errhint("The retained old data directory named by \"pg_upgrade_standby_old_datadir\" must be this standby's own pre-upgrade directory, so CN is derived at the segment the primary placed it."))); + } + + /* Window open: hold off hot standby until XLOG_UPGRADE_COMPLETE. */ + pgUpgradeReplayInProgress = true; + + /* + * Informational only: record DB_IN_UPGRADE so a crash mid-window (or + * pg_controldata) shows "in pg_upgrade" rather than the "in + * production" the arm set as its crash-recovery trigger. Does not + * drive recovery; COMPLETE restores DB_IN_PRODUCTION. + */ + if (in_upgrade_bootstrap) + SetControlFileInUpgrade(); + + /* + * Write $PGDATA/PG_VERSION from the embedded string; the top-level + * PG_VERSION is created by initdb and is not otherwise WAL-logged. + * (Per-database PG_VERSION is covered by XLOG_DBASE_CREATE_WAL_LOG.) + */ + + fd = OpenTransientFile("PG_VERSION", + O_WRONLY | O_CREAT | O_TRUNC | PG_BINARY); + if (fd < 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not open PG_VERSION: %m"))); + if (pg_pwrite(fd, xlrec->pg_version, len, 0) != len) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not write PG_VERSION: %m"))); + if (pg_fsync(fd) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not fsync PG_VERSION: %m"))); + CloseTransientFile(fd); + } + else if (info == XLOG_UPGRADE_COMPLETE) + { + /* + * Window closed and the cluster is fully reconstructed on disk. Clear + * the guard so hot standby may activate. The redo loop finishes and + * StartupXLOG() writes its end-of-recovery checkpoint (advancing the + * control checkpoint past COMPLETE), then the cluster comes up + * read-write; a streaming standby continues as an ordinary hot + * standby. + */ + pgUpgradeReplayInProgress = false; + + /* Restore the DB_IN_PRODUCTION trigger borrowed at START. */ + if (in_upgrade_bootstrap) + ClearControlFileInUpgrade(); + + /* + * Record the durable "window reached COMPLETE" flag in pg_control, + * which (paired with a control checkpoint past CN) lets the next + * startup tell a fully-upgraded cluster from a crashed partial one. + * SetControlFileUpgradeFinalized() fsyncs pg_control, so it survives + * a crash immediately after redo with no clean shutdown in between. + * It is set here, one checkpoint before StartupXLOG's end-of-recovery + * checkpoint advances the control checkpoint past CN, so a crash in + * that gap is correctly re-armed and re-replayed (see + * PerformWalUpgradeIfNeeded's finalized guard). + */ + SetControlFileUpgradeFinalized(); + } + else if (info == XLOG_UPGRADE_HANDOFF) + { + /* + * Old-format streaming-handoff trigger (see xl_pg_upgrade_handoff / + * XLogWritePgUpgradeHandoff() for why it exists and when it is + * emitted). A standby cannot follow the upgrade in the old WAL + * format, so rather than shut down it pauses recovery at the boundary + * and keeps serving read-only queries while the operator provisions a + * new-version skeleton to stream the window (this node's data + * directory is what that skeleton links user relations from). The + * pause is reversible, so an aborted upgrade is recoverable: resume + * to follow the restarted old primary, or drain to the end of the old + * WAL on the commit path. Outside StandbyMode (the old primary's own + * crash recovery) it is a no-op. + */ + if (StandbyMode) + { + xl_pg_upgrade_handoff *xlrec = + (xl_pg_upgrade_handoff *) XLogRecGetData(record); + + if (!HotStandbyActive()) + { + /* + * recoveryPausesHere() refuses to pause unless hot standby is + * active (there would be no session able to resume it). A + * caught-up streaming standby reached consistency long ago, + * so this is the unusual case; rather than silently continue + * past the boundary into WAL this old binary cannot read, + * stop cleanly here. The operator re-provisions from the + * window. + */ + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("reached pg_upgrade handoff on standby before hot standby was active; shutting down for pg_upgrade"), + errdetail("The primary initiated a --wal-upgrade to major version %u; " + "this standby cannot follow the upgrade in the old WAL format.", + xlrec->target_major_version), + errhint("Install the new-version binaries and start a fresh skeleton " + "standby with a pg_upgrade.signal file naming this data directory; " + "it will stream the upgrade window and link user relations in."))); + } + else + { + /* + * Request a recovery pause. The main redo loop honors it + * before applying the next record (xlogrecovery.c), so the + * startup process blocks at the handoff boundary while the + * postmaster and hot-standby backends keep serving read-only + * queries. pg_wal_replay_resume() (or promotion) releases + * it. + */ + ereport(LOG, + (errmsg("reached pg_upgrade handoff on standby; pausing recovery at the upgrade boundary"), + errdetail("The primary initiated a --wal-upgrade to major version %u; " + "this standby cannot follow the upgrade in the old WAL format.", + xlrec->target_major_version), + errhint("Provision a fresh new-version skeleton standby from this data directory, " + "then resume with pg_wal_replay_resume(); or, if the upgrade was aborted, " + "restart the old primary and resume to keep following it."))); + SetRecoveryPause(true); + } + } + } + else if (info == XLOG_UPGRADE_DIRTREE) + { + /* + * Rebuild the directory skeleton before any file image replays into + * it. Paths are PGDATA-relative and emitted parent-before-child, so + * one mkdir() per path suffices. Idempotent: EEXIST is expected + * (some directories already exist on disk). + */ + xl_upgrade_dirtree *xlrec = + (xl_upgrade_dirtree *) XLogRecGetData(record); + char *p = (char *) xlrec + SizeOfXLUpgradeDirtree; + char *dir_end; + char *sym_end; + uint32 done = 0; + + /* + * dir_bytes/sym_bytes are untrusted; make sure the two regions fit + * within the record before deriving end pointers from them. + */ + if ((Size) xlrec->dir_bytes + xlrec->sym_bytes > + XLogRecGetDataLen(record) - SizeOfXLUpgradeDirtree) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("dirtree record overruns the record"))); + dir_end = p + xlrec->dir_bytes; + sym_end = dir_end + xlrec->sym_bytes; + + while (p < dir_end && done < xlrec->ndirs) + { + Size plen = strnlen(p, dir_end - p); + + if (plen == 0 || p + plen >= dir_end) /* need the NUL terminator */ + break; + + if (mkdir(p, pg_dir_create_mode) != 0 && errno != EEXIST) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not create directory \"%s\": %m", + p))); + + p += plen + 1; + done++; + } + + if (done != xlrec->ndirs) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("dirtree record damaged: created %u of %u directories", + done, xlrec->ndirs))); + + /* + * Recreate captured symlinks (pg_tblspc/ -> external + * tablespace location). Each entry is two NUL-terminated strings: + * linkpath, target. Create the target directory then the symlink, so + * the tablespace exists before its RELFILE images replay. EEXIST + * tolerated. + */ + p = dir_end; + done = 0; + while (p < sym_end && done < xlrec->nsymlinks) + { + char *linkpath = p; + Size llen = strnlen(linkpath, sym_end - p); + char *target; + Size tlen; + + if (llen == 0 || p + llen >= sym_end) + break; + target = p + llen + 1; + if (target >= sym_end) + break; + tlen = strnlen(target, sym_end - target); + if (p + llen + 1 + tlen >= sym_end) + break; + + /* + * linkpath is a PGDATA-relative link (pg_tblspc/); reject an + * absolute or ".." path so a corrupt record cannot plant a + * symlink outside the data directory. (target is legitimately an + * absolute external location, so it is not constrained here.) + */ + if (linkpath[0] == '/' || strstr(linkpath, "..") != NULL) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("unsafe symlink path \"%s\"", + linkpath))); + + /* ensure the external target directory exists */ + if (mkdir(target, pg_dir_create_mode) != 0 && errno != EEXIST) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not create tablespace directory \"%s\": %m", + target))); + + if (symlink(target, linkpath) != 0 && errno != EEXIST) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not create symlink \"%s\" -> \"%s\": %m", + linkpath, target))); + + p = target + tlen + 1; + done++; + } + + if (done != xlrec->nsymlinks) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("dirtree record damaged: created %u of %u symlinks", + done, xlrec->nsymlinks))); + } + else if (info == XLOG_UPGRADE_SLRU_DATA) + { + /* + * Restore the captured SLRU segment image(s). Emitted last in + * pg_upgrade, after all transactions committed and a CHECKPOINT + * flushed the merged CLOG/multixact state, so the image carries both + * the old cluster's historical commit bits (which live only here, + * never in WAL) and the new cluster's restore statuses, and dominates + * any earlier replayed commit record for the same page. This redo is + * the sole source reconstructing pg_xact and pg_multixact. Install + * each page into the SimpleLru buffers and flush it so the + * end-of-recovery checkpoint cannot clobber it. + */ + xl_upgrade_slru_data *xlrec = + (xl_upgrade_slru_data *) XLogRecGetData(record); + char *data = (char *) xlrec + SizeOfXLUpgradeSlruData; + Size seg_size = SLRU_PAGES_PER_SEGMENT * BLCKSZ; + int64 seg; + Size off = 0; + + /* + * total_bytes is an untrusted field; validate it against the bytes + * the record actually carries before using it to bound the segment + * reads below. Otherwise a record claiming more than it holds would + * over-read the WAL buffer. + */ + if ((Size) xlrec->total_bytes > + XLogRecGetDataLen(record) - SizeOfXLUpgradeSlruData) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("SLRU record claims %u bytes but the " + "record is shorter", xlrec->total_bytes))); + + /* + * first_seg/last_seg are likewise untrusted. They feed basepage = + * seg * SLRU_PAGES_PER_SEGMENT in the restore helpers, so an + * out-of-range value would signed-overflow that product (undefined + * behavior) or install pages under a nonsensical, mis-named segment + * file. Bound them to the addressable segment range for the SLRU's + * filename width -- 2^24-1 for short names (pg_xact, pg_multixact/ + * offsets), 2^60-1 for long names (pg_multixact/members) -- and + * require a non-negative, ordered range. A valid emit never violates + * this; a violation means the record is corrupt. + */ + { + int64 max_seg; + + switch (xlrec->slru_type) + { + case UPGRADE_SLRU_XACT: + case UPGRADE_SLRU_MXOFF: + max_seg = INT64CONST(0xFFFFFF); /* short names */ + break; + case UPGRADE_SLRU_MXMEM: + max_seg = INT64CONST(0xFFFFFFFFFFFFFFF); /* long names */ + break; + default: + elog(PANIC, "invalid slru_type %u", + xlrec->slru_type); + max_seg = 0; /* keep the compiler quiet */ + } + + if (xlrec->first_seg < 0 || + xlrec->last_seg < xlrec->first_seg || + xlrec->last_seg > max_seg) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("SLRU record segment range %lld..%lld out of bounds (slru_type %u)", + (long long) xlrec->first_seg, + (long long) xlrec->last_seg, + xlrec->slru_type))); + } + + for (seg = xlrec->first_seg; seg <= xlrec->last_seg; seg++) + { + /* + * Every segment in first_seg..last_seg must be fully present. A + * short record means the WAL is damaged; silently restoring fewer + * segments would leave pg_xact/pg_multixact incomplete, so PANIC + * as the sibling handlers do rather than continue with a partial + * SLRU. + */ + if (off + seg_size > (Size) xlrec->total_bytes) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("SLRU record truncated: " + "restored %d of segments %lld..%lld (slru_type %u)", + (int) (seg - xlrec->first_seg), + (long long) xlrec->first_seg, + (long long) xlrec->last_seg, + xlrec->slru_type))); + + switch (xlrec->slru_type) + { + case UPGRADE_SLRU_XACT: + CLOGUpgradeRestoreSegment(seg, data + off, seg_size); + break; + case UPGRADE_SLRU_MXOFF: + MultiXactOffsetUpgradeRestoreSegment(seg, data + off, seg_size); + break; + case UPGRADE_SLRU_MXMEM: + MultiXactMemberUpgradeRestoreSegment(seg, data + off, seg_size); + break; + default: + elog(PANIC, "invalid slru_type %u", + xlrec->slru_type); + } + off += seg_size; + } + } + else if (info == XLOG_UPGRADE_RELFILE_DATA) + { + /* + * The record batches many relation-file chunks: + * [entry_0][data_0][entry_1][data_1] ... Restore each chunk + * page-by-page through the buffer manager. Recovery is anchored at + * CN, so these images are the sole writers of these pages (the + * on-disk file was wiped and pg_restore's WAL is not replayed). Going + * through the buffer manager lets XLogReadBufferExtended create the + * file and its directory on demand and flush the page at the + * end-of-recovery checkpoint; RBM_ZERO_AND_LOCK gives a zero-extended + * buffer we overwrite. + */ + char *ptr = XLogRecGetData(record); + char *end = ptr + XLogRecGetDataLen(record); + + while (ptr < end) + { + xl_upgrade_relfile_entry ent; + char *data; + RelFileLocator rlocator; + ForkNumber forknum; + uint32 npages; + BlockNumber base_block; + + /* + * The record is untrusted (it only had to pass a CRC check). + * Validate every offset against the record end before reading: a + * torn or corrupt record must PANIC, not over-read the WAL + * buffer. + */ + if (end - ptr < (ptrdiff_t) SizeOfXLUpgradeRelfileEntry) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("truncated relfile entry header"))); + memcpy(&ent, ptr, SizeOfXLUpgradeRelfileEntry); + ptr += SizeOfXLUpgradeRelfileEntry; + data = ptr; + if (ent.nbytes % BLCKSZ != 0 || + (Size) ent.nbytes > (Size) (end - ptr)) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("relfile entry payload (%u bytes) " + "is misaligned or overruns the record", ent.nbytes))); + ptr += ent.nbytes; + + rlocator.spcOid = ent.tablespace_oid; + rlocator.dbOid = ent.database_oid; + rlocator.relNumber = ent.relfilenumber; + forknum = (ForkNumber) ent.forknum; + + /* + * nbytes==0 means an empty relation file: create it and move on. + * Empty system catalogs (pg_publication, pg_enum, ...) have + * 0-byte relfiles; without recreating them the first write fails + * with "could not open file". + */ + if (ent.nbytes == 0) + { + SMgrRelation srel = smgropen(rlocator, INVALID_PROC_NUMBER); + + if (!smgrexists(srel, forknum)) + smgrcreate(srel, forknum, true); + smgrclose(srel); + continue; + } + + /* segments are RELSEG_SIZE blocks; blockoff is this chunk's start */ + base_block = (BlockNumber) ent.segno * RELSEG_SIZE + ent.blockoff; + npages = ent.nbytes / BLCKSZ; + + for (uint32 i = 0; i < npages; i++) + { + Buffer buffer; + Page page; + + buffer = XLogReadBufferExtended(rlocator, forknum, + base_block + i, + RBM_ZERO_AND_LOCK, + InvalidBuffer); + if (!BufferIsValid(buffer)) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read block %u of relation %u/%u/%u fork %d", + base_block + i, + rlocator.spcOid, rlocator.dbOid, + rlocator.relNumber, forknum))); + + page = BufferGetPage(buffer); + memcpy(page, data + (Size) i * BLCKSZ, BLCKSZ); + + /* + * Restamp the page LSN to this RELFILE_DATA record's own LSN, + * mirroring ordinary full-page-image replay + * (XLogRecGetBlockData / RestoreBlockImage in xlogutils.c). + * + * The captured page carries whatever LSN pg_upgrade's restore + * stamped on it, which can sit above CN. On the + * streaming-standby path CN is forced down to the old + * cluster's nextxlogfile segment (--wal-upgrade-exact), so a + * verbatim page LSN could land beyond the window and the + * end-of-recovery checkpoint would refuse to flush it ("xlog + * flush request ... not satisfied"). Stamping the record LSN + * keeps the page within the window (>= CN) and thus + * flushable. The physical-equivalence test masks pd_lsn, so + * this does not regress the byte-match-modulo-LSN guarantee. + * + * An uninitialized (all-zero) page must not be stamped -- + * PageSetLSN on a new page corrupts the PageIsNew() invariant + * (see xlogutils.c RestoreBlockImage). + */ + if (!PageIsNew(page)) + { + PageSetLSN(page, record->EndRecPtr); + + /* + * PageSetLSN mutated pd_lsn, invalidating the pd_checksum + * carried in the captured image. Recompute it in place + * so the in-memory page is self-consistent before it can + * reach disk. PageSetChecksum is a no-op when checksums + * are off or the page is new, so this is safe and cheap. + */ + PageSetChecksum(page, base_block + i); + } + MarkBufferDirty(buffer); + UnlockReleaseBuffer(buffer); + } + } + } + else if (info == XLOG_UPGRADE_RELINK) + { + /* + * Manifest of user relation files the window omits. On a streaming + * standby, link each from this node's retained old datadir into the + * new skeleton at the identical relative path -- pg_upgrade preserves + * relfilenumbers and db/tablespace OIDs, so old and new paths match. + * This is the standby's equivalent of the primary's + * transfer_relfile() step, driven by replay. + * + * The old datadir path is standby-local, supplied via the + * pg_upgrade_standby_old_datadir GUC (see + * ArmFromLocalDerivationIfConfigured); read it here. When it is + * unset -- on the primary (which already has the files) and on + * archive/PITR recovery (the old files arrived with the base backup) + * -- there is nothing to do. A streaming standby that reaches this + * record with no path cannot materialize user data, so it FATALs (see + * below). + */ + char *ptr = XLogRecGetData(record); + char *end = ptr + XLogRecGetDataLen(record); + const char *old_datadir = pg_upgrade_standby_old_datadir; + bool have_old_datadir = (old_datadir != NULL && + old_datadir[0] != '\0'); + + if (!have_old_datadir) + { + /* + * A streaming-standby skeleton (armed_streaming_standby) is empty + * of user data, so reaching the manifest with no old-datadir path + * to link from means every user relation would silently be absent + * -- a misconfiguration, not a no-op. Refuse to continue rather + * than bring up a hot standby that is missing all its user data. + * (On the primary's own crash recovery and on archive/PITR the + * files are already on disk, so the same manifest is a legitimate + * no-op and we just move on.) + */ + if (armed_streaming_standby) + ereport(FATAL, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("reached the relink manifest but \"pg_upgrade_standby_old_datadir\" is not set"), + errhint("Set \"pg_upgrade_standby_old_datadir\" in postgresql.conf to this standby's retained pre-upgrade data directory so user relations can be linked in."))); + } + else + { + /* + * The standby may place user relations with a different transfer + * mode than the primary used: pg_upgrade_standby_transfer_mode + * overrides the per-file mode recorded in the manifest. + * PG_UPGRADE_XFER_MIRROR (the default) keeps the primary's + * per-file mode; anything else forces that one mode for every + * file (e.g. the primary linked to save space, but this standby + * wants independent copies so its old datadir stays a bootable + * rollback target). Resolved once per record; whether it is + * applied is decided per entry below. + */ + bool override_mode = + (pg_upgrade_standby_transfer_mode != PG_UPGRADE_XFER_MIRROR); + uint8 forced_mode = (uint8) pg_upgrade_standby_transfer_mode; + + if (override_mode) + ereport(DEBUG1, + (errmsg("overriding relink transfer mode with pg_upgrade_standby_transfer_mode=%d", + pg_upgrade_standby_transfer_mode))); + + while (ptr < end) + { + xl_upgrade_relink_entry ent; + RelFileLocator rlocator; + RelPathStr relpath; + char oldfile[MAXPGPATH]; + char newfile[MAXPGPATH]; + char segsuffix[32]; + char *slash; + struct stat oldstat; + uint8 place_mode; + + if (end - ptr < (ptrdiff_t) SizeOfXLUpgradeRelinkEntry) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("truncated relink entry"))); + memcpy(&ent, ptr, SizeOfXLUpgradeRelinkEntry); + ptr += SizeOfXLUpgradeRelinkEntry; + + /* + * The record is untrusted (a corrupt-but-CRC-valid record + * could carry any byte values). Bound the fields we index or + * branch on before use: forknum indexes forkNames[] + * (0..MAX_FORKNUM). + */ + if (ent.forknum > MAX_FORKNUM) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("relink entry fork number %u out of range", + ent.forknum))); + + /* + * Effective placement mode. Only "mirror" consults the + * manifest tuple's transfer_mode -- so only then do we read + * and bounds-check it (it selects a placement primitive). + * With an operator override the manifest's mode is irrelevant + * and left untouched; forced_mode comes from a validated GUC + * enum, so it is always in range. + */ + if (override_mode) + place_mode = forced_mode; + else + { + if (ent.transfer_mode > UPGRADE_RELINK_MODE_SWAP) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("relink entry transfer mode %u out of range", + ent.transfer_mode))); + place_mode = ent.transfer_mode; + } + + rlocator.spcOid = ent.tablespace_oid; + rlocator.dbOid = ent.database_oid; + rlocator.relNumber = ent.relfilenumber; + relpath = GetRelationPath(rlocator.dbOid, rlocator.spcOid, + rlocator.relNumber, + INVALID_PROC_NUMBER, + (ForkNumber) ent.forknum); + + /* segment 0 has no suffix; higher segments are ".N" */ + if (ent.segno == 0) + segsuffix[0] = '\0'; + else + snprintf(segsuffix, sizeof(segsuffix), ".%u", ent.segno); + + /* + * The destination path is version-stable (this binary built + * both relpath and DataDir), but the source path in the + * retained old datadir uses the OLD version's tablespace + * directory for a user-tablespace relation -- resolve it (see + * RelinkBuildOldFile). + */ + if (!RelinkBuildOldFile(old_datadir, rlocator.spcOid, + relpath.str, segsuffix, + oldfile, sizeof(oldfile))) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not resolve the old-cluster source path for tablespace %u relation \"%s\"", + rlocator.spcOid, relpath.str), + errhint("The retained old data directory named by \"pg_upgrade_standby_old_datadir\" must contain this standby's pre-upgrade tablespace directory."))); + snprintf(newfile, sizeof(newfile), "%s/%s%s", + DataDir, relpath.str, segsuffix); + + /* + * Create the destination's parent dir (base// etc.); + * the skeleton's initdb only made the built-in databases, and + * the window's DIRTREE record may not have arrived yet. + */ + slash = strrchr(newfile, '/'); + if (slash != NULL) + { + *slash = '\0'; + if (MakePGDirectory(newfile) != 0 && errno != EEXIST) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not create directory \"%s\": %m", + newfile))); + *slash = '/'; + } + + /* + * Place the file, reproducing pg_upgrade's transfer mode (see + * RelinkPlaceFile for the per-mode taxonomy). + * + * The manifest is authoritative for user data, so a + * destination that already exists -- an empty placeholder a + * RELFILE image created for a same-numbered relation, or a + * prior replay of this record -- is removed and replaced. A + * source that has since vanished (ENOENT) is skipped. + */ + if (stat(oldfile, &oldstat) != 0) + { + if (errno == ENOENT) + continue; /* source gone; nothing to place */ + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not stat \"%s\": %m", + oldfile))); + } + unlink(newfile); + RelinkPlaceFile(oldfile, newfile, place_mode); + + /* + * Make the new directory entry durable. RelinkPlaceFile() + * fsync'd the file contents; fsyncing the parent directory + * here persists the dentry (the new hardlink for --link, the + * moved entry for --swap) before COMPLETE advances the + * control file past CN. + */ + slash = strrchr(newfile, '/'); + if (slash != NULL) + { + *slash = '\0'; + fsync_fname(newfile, true); + *slash = '/'; + } + } + } + } + else if (info == XLOG_UPGRADE_RAWFILE) + { + /* + * Write a verbatim non-relation file (pg_filenode.map, PG_VERSION), + * creating any missing parent directory. These files are not + * reachable through the buffer manager, so this is the only way to + * rebuild the relation map and version stamps from an otherwise-empty + * data directory. + */ + xl_upgrade_rawfile *xlrec = + (xl_upgrade_rawfile *) XLogRecGetData(record); + char *payload = (char *) xlrec + SizeOfXLUpgradeRawfile; + char path[MAXPGPATH]; + char *data = payload + xlrec->path_len; + int fd; + char *slash; + + /* + * The record is untrusted. Validate path_len + data_len against the + * bytes actually present before reading either, and require the path + * to be a safe PGDATA-relative path (no absolute path, no "..") so a + * corrupt or hostile record cannot write outside the data directory. + */ + if (xlrec->path_len >= MAXPGPATH) + elog(PANIC, "rawfile path too long"); + if ((Size) xlrec->path_len + xlrec->data_len > + XLogRecGetDataLen(record) - SizeOfXLUpgradeRawfile) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("rawfile record overruns the record"))); + memcpy(path, payload, xlrec->path_len); + path[xlrec->path_len] = '\0'; + if (path[0] == '/' || strstr(path, "..") != NULL) + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("unsafe rawfile path \"%s\"", path))); + + /* create parent directory if it does not exist (e.g. base/) */ + slash = strrchr(path, '/'); + if (slash != NULL) + { + *slash = '\0'; + if (mkdir(path, pg_dir_create_mode) != 0 && errno != EEXIST) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not create directory \"%s\": %m", + path))); + *slash = '/'; + } + + fd = OpenTransientFile(path, O_WRONLY | O_CREAT | O_TRUNC | PG_BINARY); + if (fd < 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not open \"%s\": %m", path))); + if (pg_pwrite(fd, data, xlrec->data_len, 0) != (ssize_t) xlrec->data_len) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not write \"%s\": %m", path))); + if (pg_fsync(fd) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not fsync \"%s\": %m", path))); + CloseTransientFile(fd); + } + else + elog(PANIC, "unknown op code %u", info); +} diff --git a/src/backend/access/transam/rmgr.c b/src/backend/access/transam/rmgr.c index 4fda03a3cf..305ada93e8 100644 --- a/src/backend/access/transam/rmgr.c +++ b/src/backend/access/transam/rmgr.c @@ -39,6 +39,7 @@ #include "replication/message.h" #include "replication/origin.h" #include "storage/standby.h" +#include "access/pgupgrade_wal.h" /* RM_PG_UPGRADE_ID */ #include "utils/relmapper.h" /* IWYU pragma: end_keep */ diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c index 47dd52d674..b6ca7682c4 100644 --- a/src/backend/access/transam/slru.c +++ b/src/backend/access/transam/slru.c @@ -1443,6 +1443,77 @@ SimpleLruWriteAll(SlruDesc *ctl, bool allow_redirtied) fsync_fname(ctl->options.Dir, true); } +/* + * Restore one SLRU segment's worth of pages from a captured image during + * pg_upgrade --wal-upgrade WAL replay (XLOG_UPGRADE_SLRU_DATA redo). + * + * "segno" is the SLRU segment number; "data" is the raw segment image and + * "datalen" its length (a whole number of BLCKSZ pages, padded to a full + * segment by the emit side). For each page the captured image is installed + * into a SimpleLru buffer, marked dirty, and flushed to disk immediately. + * + * The image is the final, merged CLOG/multixact state captured after the whole + * upgrade completed, so it is authoritative: it dominates anything an earlier + * replayed commit record wrote for the same page (this record is emitted last) + * and it does not depend on the on-disk SLRU segment file, which pg_upgrade has + * unlinked as part of skipping the disk writes. + * + * The SLRU directory (ctl->Dir) is recreated earlier in replay by the + * XLOG_UPGRADE_DIRTREE record (the logged after-image of the initdb directory + * tree), so it is guaranteed to exist by the time we write a segment here. + */ +void +SlruUpgradeRestoreSegment(SlruDesc *ctl, int64 segno, + const char *data, Size datalen) +{ + int npages = (int) (datalen / BLCKSZ); + int64 basepage = segno * SLRU_PAGES_PER_SEGMENT; + SlruWriteAllData fdata; + + /* + * Write the whole segment through a single SlruWriteAll batch so the + * segment file is opened once, not once per page. Passing &fdata to + * SlruInternalWritePage makes SlruPhysicalWritePage cache and reuse the + * open fd across all of this segment's pages (exactly as + * SimpleLruWriteAll does at checkpoint); we close it once at the end. All + * pages of one segment share a bank on well-configured SLRUs, but acquire + * the correct bank lock per page to stay correct regardless of bank + * layout. + */ + fdata.num_files = 0; + + for (int i = 0; i < npages; i++) + { + int64 pageno = basepage + i; + LWLock *lock = SimpleLruGetBankLock(ctl, pageno); + int slotno; + + LWLockAcquire(lock, LW_EXCLUSIVE); + + /* allocate/zero a buffer slot for this page, then overwrite it */ + slotno = SimpleLruZeroPage(ctl, pageno); /* also marks the slot + * dirty */ + memcpy(ctl->shared->page_buffer[slotno], + data + (Size) i * BLCKSZ, BLCKSZ); + + /* persist through the batch (reuses the cached fd for this segment) */ + SlruInternalWritePage(ctl, slotno, &fdata); + + LWLockRelease(lock); + } + + /* Close the fd(s) the batch left open. */ + for (int i = 0; i < fdata.num_files; i++) + { + if (CloseTransientFile(fdata.fd[i]) != 0) + { + slru_errcause = SLRU_CLOSE_FAILED; + slru_errno = errno; + SlruReportIOError(ctl, basepage, NULL); + } + } +} + /* * Remove all segments before the one holding the passed page number * diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index a81912b744..1c364c0f54 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -46,7 +46,10 @@ #include #include -#include "access/clog.h" +#include "access/clog.h" /* CheckPointCLOG for XLogFlushUpgradeSLRU */ +#include "access/pgupgrade_wal.h" /* RM_PG_UPGRADE_ID, pg_upgrade WAL + * functions */ +#include "access/slru.h" /* SLRU_PAGES_PER_SEGMENT for upgrade WAL */ #include "access/commit_ts.h" #include "access/heaptoast.h" #include "access/multixact.h" @@ -86,6 +89,7 @@ #include "replication/walreceiver.h" #include "replication/walsender.h" #include "storage/bufmgr.h" +#include "storage/copydir.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/large_object.h" @@ -143,6 +147,10 @@ int max_slot_wal_keep_size_mb = -1; int wal_decode_buffer_size = 512 * 1024; bool track_wal_io_timing = false; +/* pg_upgrade --wal-upgrade streaming-standby options (see xlog.h) */ +int pg_upgrade_standby_transfer_mode = PG_UPGRADE_XFER_MIRROR; +char *pg_upgrade_standby_old_datadir = NULL; + #ifdef WAL_DEBUG bool XLOG_DEBUG = false; #endif @@ -208,6 +216,21 @@ const struct config_enum_entry archive_mode_options[] = { {NULL, 0, false} }; +/* + * pg_upgrade_standby_transfer_mode options. "mirror" (the default) means reproduce the + * per-file mode the primary recorded in the RELINK manifest; the rest override + * it with a specific placement primitive on the standby. + */ +const struct config_enum_entry pg_upgrade_standby_transfer_mode_options[] = { + {"mirror", PG_UPGRADE_XFER_MIRROR, false}, + {"clone", PG_UPGRADE_XFER_CLONE, false}, + {"copy", PG_UPGRADE_XFER_COPY, false}, + {"copy_file_range", PG_UPGRADE_XFER_COPY_FILE_RANGE, false}, + {"link", PG_UPGRADE_XFER_LINK, false}, + {"swap", PG_UPGRADE_XFER_SWAP, false}, + {NULL, 0, false} +}; + /* * Statistics for current checkpoint are collected in this global struct. * Because only the checkpointer or a stand-alone backend can perform @@ -4402,6 +4425,221 @@ WriteControlFile(void) XLOG_CONTROL_FILE))); } +/* + * Synthesize a minimal, valid global/pg_control (and PG_VERSION) from this + * binary's compile-time constants so a --wal-upgrade recovery can start without + * initdb. Only the compatibility-check fields must be right; the run-time + * fields are fixed up afterward by ArmControlFileForUpgradeRecovery(). + * + * allow_overwrite=false creates the file O_EXCL (streaming-standby skeleton, + * never clobber an existing one); allow_overwrite=true uses O_TRUNC to replace + * the old-version pg_control left by archive-PITR recovery. + * + * Runs in the postmaster before CreateSharedMemoryAndSemaphores(), so there is + * no shared ControlFile yet; a local buffer is built and written directly. + */ +void +SynthesizeUpgradeStreamControlFile(bool allow_overwrite) +{ + ControlFileData *cf; + char buffer[PG_CONTROL_FILE_SIZE]; /* need not be aligned */ + char verpath[MAXPGPATH]; + char globaldir[MAXPGPATH]; + char ctlpath[MAXPGPATH]; + int fd; + char mock_auth_nonce[MOCK_AUTH_NONCE_LEN]; + + /* + * The CWD is not yet the data directory at this point, so build absolute + * paths from DataDir rather than relying on relative names. + */ + snprintf(globaldir, sizeof(globaldir), "%s/global", DataDir); + snprintf(ctlpath, sizeof(ctlpath), "%s/%s", DataDir, XLOG_CONTROL_FILE); + + /* + * On the overwrite (archive-PITR) path, keep an existing valid + * current-version control file. A same-version PITR restore's base + * backup carries a good new-version pg_control, and clobbering it would + * discard its data_checksum_version (synthesis hardcodes 0), so a + * checksummed cluster would then fail page verification during replay. + * Synthesize only when the existing file is absent or a different major's + * (the real cross-version case). + */ + if (allow_overwrite) + { + struct stat st; + + if (stat(ctlpath, &st) == 0) + { + bool existing_crc_ok = false; + ControlFileData *existing = get_controlfile(DataDir, &existing_crc_ok); + + if (existing != NULL) + { + bool usable = (existing_crc_ok && + existing->pg_control_version == PG_CONTROL_VERSION && + existing->catalog_version_no == CATALOG_VERSION_NO); + + pfree(existing); + if (usable) + { + ereport(LOG, + (errmsg("keeping the existing new-version control file for upgrade-stream recovery"), + errdetail("A valid same-version pg_control is present; not synthesizing over it (preserves its data checksum state)."))); + return; + } + } + } + } + + cf = (ControlFileData *) palloc0(sizeof(ControlFileData)); + + if (!pg_strong_random(mock_auth_nonce, MOCK_AUTH_NONCE_LEN)) + ereport(FATAL, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not generate secret authorization token"))); + + /* + * Status fields. system_identifier is a placeholder (0); the arm + * overwrites it with the WAL's sysid later. DB_SHUTDOWNED is what the + * arm expects. + */ + cf->system_identifier = 0; + memcpy(cf->mock_authentication_nonce, mock_auth_nonce, MOCK_AUTH_NONCE_LEN); + cf->state = DB_SHUTDOWNED; + cf->unloggedLSN = FirstNormalUnloggedLSN; + + /* + * Parameter values used when replaying WAL, from this node's GUCs (its + * own postgresql.conf or the built-in defaults, not the primary's). As + * for any standby, these must be at least the primary's values or + * recovery aborts "because of insufficient parameter settings" when the + * window's PARAMETER_CHANGE record replays. + */ + cf->MaxConnections = MaxConnections; + cf->max_worker_processes = max_worker_processes; + cf->max_wal_senders = max_wal_senders; + cf->max_prepared_xacts = max_prepared_xacts; + cf->max_locks_per_xact = max_locks_per_xact; + cf->wal_level = wal_level; + cf->wal_log_hints = wal_log_hints; + cf->track_commit_timestamp = track_commit_timestamp; + cf->data_checksum_version = 0; + + /* Version and compatibility-check fields (all compile-time constants). */ + cf->pg_control_version = PG_CONTROL_VERSION; + cf->catalog_version_no = CATALOG_VERSION_NO; + cf->maxAlign = MAXIMUM_ALIGNOF; + cf->floatFormat = FLOATFORMAT_VALUE; + cf->blcksz = BLCKSZ; + cf->relseg_size = RELSEG_SIZE; + cf->slru_pages_per_segment = SLRU_PAGES_PER_SEGMENT; + cf->xlog_blcksz = XLOG_BLCKSZ; + cf->xlog_seg_size = wal_segment_size; + cf->nameDataLen = NAMEDATALEN; + cf->indexMaxKeys = INDEX_MAX_KEYS; + cf->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE; + cf->loblksize = LOBLKSIZE; + cf->float8ByVal = true; /* vestigial */ + cf->default_char_signedness = true; + + /* CRC over the struct, exactly as WriteControlFile() does. */ + INIT_CRC32C(cf->crc); + COMP_CRC32C(cf->crc, cf, offsetof(ControlFileData, crc)); + FIN_CRC32C(cf->crc); + + memset(buffer, 0, PG_CONTROL_FILE_SIZE); + memcpy(buffer, cf, sizeof(ControlFileData)); + + /* + * Create the standard cluster subdirectories initdb would make; startup + * opens some before replaying the window's DIRTREE record. Mirrors + * initdb.c's subdirs[]; EEXIST is tolerated. + */ + { + static const char *const subdirs[] = { + "global", "base", "pg_wal", "pg_wal/archive_status", "pg_wal/summaries", + "pg_commit_ts", "pg_dynshmem", "pg_notify", "pg_serial", + "pg_snapshots", "pg_subtrans", "pg_twophase", + "pg_multixact", "pg_multixact/members", "pg_multixact/offsets", + "pg_replslot", "pg_tblspc", "pg_stat", "pg_stat_tmp", "pg_xact", + "pg_logical", "pg_logical/snapshots", "pg_logical/mappings" + }; + + for (int s = 0; s < (int) lengthof(subdirs); s++) + { + char dpath[MAXPGPATH]; + + snprintf(dpath, sizeof(dpath), "%s/%s", DataDir, subdirs[s]); + if (MakePGDirectory(dpath) != 0 && errno != EEXIST) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not create directory \"%s\": %m", dpath))); + } + } + + /* + * Write global/pg_control: O_EXCL for a streaming standby (must not + * clobber), O_TRUNC for archive-PITR recovery (must replace the + * old-version file). + */ + fd = BasicOpenFile(ctlpath, + O_RDWR | O_CREAT | PG_BINARY | + (allow_overwrite ? O_TRUNC : O_EXCL)); + if (fd < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not create file \"%s\": %m", ctlpath))); + errno = 0; + if (write(fd, buffer, PG_CONTROL_FILE_SIZE) != PG_CONTROL_FILE_SIZE) + { + if (errno == 0) + errno = ENOSPC; + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": %m", ctlpath))); + } + if (pg_fsync(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", ctlpath))); + if (close(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", ctlpath))); + + /* + * Write a matching PG_VERSION (major-version stamp checkDataDir + * requires). + */ + snprintf(verpath, sizeof(verpath), "%s/PG_VERSION", DataDir); + fd = BasicOpenFile(verpath, O_RDWR | O_CREAT | O_TRUNC | PG_BINARY); + if (fd < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not create file \"%s\": %m", verpath))); + if (write(fd, PG_MAJORVERSION "\n", strlen(PG_MAJORVERSION) + 1) != + (int) (strlen(PG_MAJORVERSION) + 1)) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": %m", verpath))); + if (pg_fsync(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", verpath))); + if (close(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", verpath))); + + pfree(cf); + + ereport(LOG, + (errmsg("synthesized a fresh pg_control and PG_VERSION for an upgrade-stream standby"), + errdetail("The standby will stream and replay the upgrade window from its primary; no initdb was required."))); +} + + static void ReadControlFile(void) { @@ -4636,6 +4874,171 @@ UpdateControlFile(void) update_controlfile(DataDir, ControlFile, true); } +/* + * Arm the control file for pg_upgrade --wal-upgrade recovery. + * + * Points checkPoint at CN (the end-of-upgrade checkpoint) and forces wal_level + * to replica so recovery replays from CN through XLOG_UPGRADE_COMPLETE. State + * and minRecoveryPoint differ by mode (see below). Called before StartupXLOG() + * reads checkPointCopy, so the update takes effect this recovery cycle. + */ +void +ArmControlFileForUpgradeRecovery(const struct CheckPoint *cn, XLogRecPtr cn_lsn, + uint64 wal_sysid, bool for_streaming) +{ + Assert(ControlFile != NULL); + + ControlFile->checkPoint = cn_lsn; + ControlFile->checkPointCopy = *cn; + ControlFile->wal_level = WAL_LEVEL_REPLICA; + ControlFile->minRecoveryPointTLI = cn->ThisTimeLineID; + + if (for_streaming) + { + /* + * Streaming standby: CN is not on local disk yet; it must stream in + * from the primary. InitWalRecovery() enters standby mode only when + * minRecoveryPoint is valid or state == DB_SHUTDOWNED, so set both. + */ + ControlFile->state = DB_SHUTDOWNED; + ControlFile->minRecoveryPoint = cn_lsn; + } + else + { + /* + * Local-window arm: the whole window is already in pg_wal/, so + * recover it as ordinary crash recovery, stopping at the end of + * available WAL. + */ + ControlFile->state = DB_IN_PRODUCTION; + ControlFile->minRecoveryPoint = InvalidXLogRecPtr; + ControlFile->minRecoveryPointTLI = 0; + } + + /* + * Adopt the sysid the upgrade WAL was emitted under so recovery's + * per-page xlp_sysid checks accept the burst. wal_sysid==0 means the + * scan could not read it; leave pg_control untouched. + */ + if (wal_sysid != 0) + ControlFile->system_identifier = wal_sysid; + + UpdateControlFile(); +} + +/* + * Informational state flips bracketing the upgrade-window replay: redo calls + * these at XLOG_UPGRADE_START and XLOG_UPGRADE_COMPLETE so a crash mid-window + * (or pg_controldata) shows "in pg_upgrade". They do not affect the + * recovery-mode decision; Clear restores DB_IN_PRODUCTION. + */ +void +SetControlFileInUpgrade(void) +{ + Assert(ControlFile != NULL); + + ControlFile->state = DB_IN_UPGRADE; + ControlFile->time = (pg_time_t) time(NULL); + UpdateControlFile(); +} + +void +ClearControlFileInUpgrade(void) +{ + Assert(ControlFile != NULL); + + if (ControlFile->state == DB_IN_UPGRADE) + { + ControlFile->state = DB_IN_PRODUCTION; + ControlFile->time = (pg_time_t) time(NULL); + UpdateControlFile(); + } +} + +/* + * The checkpoint LSN currently recorded in the control file. Used by + * PerformWalUpgradeIfNeeded() as the timeline-independent "already applied?" + * signal: once first startup finalizes, this advances past XLOG_UPGRADE_COMPLETE, + * whereas a pending upgrade still points at/before CN (which precedes COMPLETE). + */ +XLogRecPtr +GetControlFileCheckPointLSN(void) +{ + Assert(ControlFile != NULL); + return ControlFile->checkPoint; +} + +/* + * --wal-upgrade: durable "upgrade window replayed to COMPLETE" flag. Set (and + * fsync'd) by the XLOG_UPGRADE_COMPLETE redo handler the instant the window + * finishes -- one checkpoint BEFORE the end-of-recovery checkpoint advances the + * control checkpoint past CN -- so that a crash in that gap leaves the flag set + * with the checkpoint still at CN, which PerformWalUpgradeIfNeeded() treats as + * "re-arm and re-replay" (idempotent), not "finalized". The flag lives in + * pg_control so it is updated durably in the same write as the rest of the + * control-file state. + */ +void +SetControlFileUpgradeFinalized(void) +{ + Assert(ControlFile != NULL); + + /* + * Called from two contexts: the XLOG_UPGRADE_COMPLETE redo handler + * (startup process, single-threaded recovery) and EmitUpgradeWalWindow() + * on the live burst server (an ordinary backend, concurrent with the + * checkpointer). Take ControlFileLock so the latter is safe; the former's + * acquire is uncontended. + */ + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + if (!ControlFile->upgrade_finalized) + { + ControlFile->upgrade_finalized = true; + UpdateControlFile(); + } + LWLockRelease(ControlFileLock); +} + +bool +GetControlFileUpgradeFinalized(void) +{ + Assert(ControlFile != NULL); + return ControlFile->upgrade_finalized; +} + +/* + * --wal-upgrade: durable "an upgrade window has been started here" flag. Set on + * the burst server just before XLOG_UPGRADE_START is emitted, so a crash before + * COMPLETE leaves a durable trace (upgrade_started && !upgrade_finalized) that + * first-startup can refuse on, independent of whether the START-bearing WAL + * survived recycling. See the field comment in pg_control.h. + */ +void +SetControlFileUpgradeStarted(void) +{ + Assert(ControlFile != NULL); + + /* + * Like SetControlFileUpgradeFinalized(): may run on the live burst server + * (an ordinary backend, concurrent with the checkpointer), so take the + * lock. + */ + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + if (!ControlFile->upgrade_started) + { + ControlFile->upgrade_started = true; + UpdateControlFile(); + } + LWLockRelease(ControlFileLock); +} + +bool +GetControlFileUpgradeStarted(void) +{ + Assert(ControlFile != NULL); + return ControlFile->upgrade_started; +} + /* * Returns the unique system identifier from control file. */ @@ -5932,6 +6335,22 @@ StartupXLOG(void) timebuf, sizeof(timebuf))))); break; + case DB_IN_UPGRADE: + + /* + * Crashed while replaying the upgrade window (informational + * state). PerformWalUpgradeIfNeeded() already re-armed at CN + * before this point (resetting the state to DB_IN_PRODUCTION), so + * this case is reached only if that arming was skipped; report it + * honestly and let recovery proceed rather than FATAL as an + * "invalid" state. + */ + ereport(LOG, + (errmsg("database system was interrupted while replaying a pg_upgrade window (last known up at %s)", + str_time(ControlFile->time, + timebuf, sizeof(timebuf))))); + break; + default: ereport(FATAL, (errcode(ERRCODE_DATA_CORRUPTED), @@ -6561,6 +6980,16 @@ StartupXLOG(void) if (performedWalRecovery) promoted = PerformRecoveryXLogAction(); + /* + * The --wal-upgrade new cluster comes up read-write on first start, as + * ordinary pg_upgrade does. Atomicity is enforced earlier: + * PerformWalUpgradeIfNeeded() FATALs on a partial + * (START-without-COMPLETE) local window before arming, so only a + * fully-replayed window reaches this point and serves. If the upgrade + * result is unsatisfactory, the operator falls back to the untouched old + * cluster (or a standby). + */ + /* * If any of the critical GUCs have changed, log them before we allow * backends to write WAL. @@ -7116,6 +7545,21 @@ ShutdownXLOG(int code, Datum arg) ereport(IsPostmasterEnvironment ? LOG : NOTICE, (errmsg("shutting down"))); + /* + * pg_upgrade --wal-upgrade-signal-handoff: if it armed a handoff, emit + * the XLOG_UPGRADE_HANDOFF trigger now, while WAL insertion is still + * permitted and WAL senders are still active to stream it. This runs + * after the postmaster has drained all backends (fast shutdown waits for + * PM_WAIT_BACKENDS before invoking the shutdown checkpoint), so every + * user commit is already flushed at an LSN below the handoff; the + * shutdown checkpoint that follows lands above it. The handoff is + * therefore the exact end of the old WAL stream a streaming standby stops + * at, with no commit able to slip past it. Only a live primary emits (a + * standby replaying this record must not re-emit). + */ + if (!RecoveryInProgress()) + EmitPgUpgradeHandoffIfArmed(); + /* * Signal walsenders to move to stopping state. */ @@ -8667,6 +9111,849 @@ XLogAssignLSN(void) return XLogInsert(RM_XLOG_ID, XLOG_ASSIGN_LSN); } +/* + * Write a WAL record marking the start (XLOG_UPGRADE_START) or completion + * (XLOG_UPGRADE_COMPLETE) of pg_upgrade. These records bracket the + * schema-restore window; both markers present means the upgrade was atomic. + */ +XLogRecPtr +XLogWritePgUpgrade(bool is_start, uint32 old_major_version, + uint32 new_major_version) +{ + xl_pg_upgrade xlrec; + XLogRecPtr RecPtr; + + /* zero padding and the pg_version tail so no stack bytes leak into WAL */ + memset(&xlrec, 0, sizeof(xlrec)); + xlrec.old_major_version = old_major_version; + xlrec.new_major_version = new_major_version; + xlrec.upgrade_time = (pg_time_t) time(NULL); + /* Embed PG_MAJORVERSION so redo can write $PGDATA/PG_VERSION */ + snprintf(xlrec.pg_version, sizeof(xlrec.pg_version), "%s\n", + PG_MAJORVERSION); + + XLogBeginInsert(); + XLogRegisterData(&xlrec, SizeOfXLPgUpgrade); + RecPtr = XLogInsert(RM_PG_UPGRADE_ID, + is_start ? XLOG_UPGRADE_START + : XLOG_UPGRADE_COMPLETE); + + if (is_start) + ereport(LOG, + (errmsg("recorded pg_upgrade start at %X/%X (old major version %u, new major version %u)", + LSN_FORMAT_ARGS(RecPtr), + old_major_version, new_major_version))); + else + ereport(LOG, + (errmsg("recorded pg_upgrade completion at %X/%X (old major version %u, new major version %u)", + LSN_FORMAT_ARGS(RecPtr), + old_major_version, new_major_version))); + + return RecPtr; +} + +/* + * XLogWritePgUpgradeHandoff -- emit the OLD-format streaming-handoff trigger. + * + * Called from ShutdownXLOG() on the OLD primary as it shuts down (see + * EmitPgUpgradeHandoffIfArmed), so the record is written in the OLD WAL page + * format, streamed to a physical standby still following it, and guaranteed to + * sit after all user WAL and before the shutdown checkpoint. See + * xl_pg_upgrade_handoff for why this is a separate record from the new-format + * XLOG_UPGRADE_START burst. + */ +XLogRecPtr +XLogWritePgUpgradeHandoff(uint32 old_major_version, uint32 target_major_version) +{ + xl_pg_upgrade_handoff xlrec; + XLogRecPtr RecPtr; + + /* zero padding so no uninitialized stack bytes leak into WAL */ + memset(&xlrec, 0, sizeof(xlrec)); + xlrec.old_major_version = old_major_version; + xlrec.target_major_version = target_major_version; + xlrec.handoff_time = (pg_time_t) time(NULL); + + XLogBeginInsert(); + XLogRegisterData(&xlrec, SizeOfXLPgUpgradeHandoff); + RecPtr = XLogInsert(RM_PG_UPGRADE_ID, XLOG_UPGRADE_HANDOFF); + + /* + * Flush it: a streaming standby must receive this record before the + * primary shuts down, so it must be on disk, not buffered. + */ + XLogFlush(RecPtr); + + ereport(LOG, + (errmsg("pg_upgrade handoff trigger recorded at %X/%X " + "(old major version %u, target major version %u)", + LSN_FORMAT_ARGS(RecPtr), + old_major_version, target_major_version))); + + return RecPtr; +} + +/* + * EmitPgUpgradeHandoffIfArmed -- emit the streaming-handoff trigger at shutdown + * if pg_upgrade --wal-upgrade-signal-handoff armed it. + * + * Called from ShutdownXLOG() on a live primary, before WAL senders stop and + * before the shutdown checkpoint. The sentinel file (written by the pg_upgrade + * lifecycle subcommand) holds the target major version. Absent sentinel is the + * common case and a no-op. The file is removed once consumed so a later restart + * does not re-emit. + */ +void +EmitPgUpgradeHandoffIfArmed(void) +{ + const char *path = PG_UPGRADE_HANDOFF_SIGNAL_FILE; + FILE *f; + int target_major = 0; + + f = AllocateFile(path, "r"); + if (f == NULL) + { + if (errno != ENOENT) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not open pg_upgrade handoff signal file \"%s\": %m", + path))); + return; /* not armed: normal shutdown */ + } + + if (fscanf(f, "%d", &target_major) != 1 || target_major <= 0) + { + FreeFile(f); + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid data in pg_upgrade handoff signal file \"%s\"", + path))); + } + FreeFile(f); + + (void) XLogWritePgUpgradeHandoff(PG_VERSION_NUM / 10000, + (uint32) target_major); + + if (unlink(path) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not remove pg_upgrade handoff signal file \"%s\": %m", + path))); +} + +/* + * Recursively collect PGDATA-relative subdirectory paths under "abspath" + * (relative prefix "relpath"), appending each NUL-terminated to dbuf and + * counting them in *ndirs. Parents are appended before children so a plain + * mkdir() per path suffices on replay. + * + * pg_wal is skipped; the upgrade WAL segments and pg_wal/ are handled by + * PerformWalUpgradeIfNeeded(), not by the directory after-image. + */ +static void +CollectUpgradeDirs(const char *abspath, const char *relpath, + StringInfo dbuf, uint32 *ndirs, + StringInfo sbuf, uint32 *nsymlinks) +{ + DIR *dir; + struct dirent *de; + + dir = AllocateDir(abspath); + if (dir == NULL) + return; + + while ((de = ReadDir(dir, abspath)) != NULL) + { + char childabs[MAXPGPATH]; + char childrel[MAXPGPATH]; + struct stat st; + + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + + /* top-level pg_wal is not part of the after-image (see comment) */ + if (relpath[0] == '\0' && strcmp(de->d_name, "pg_wal") == 0) + continue; + + /* + * Top-level pg_replslot is not part of the after-image either. Its + * upgrade-window retention slot has a per-slot "state" file that is + * not WAL-logged, so capturing the directory without it would leave a + * reconstructed cluster with an empty pg_replslot// that PANICs + * at startup. The slot is a primary-only artifact; a reconstructed + * cluster starts with an empty pg_replslot/, so skip the whole + * subtree. + */ + if (relpath[0] == '\0' && strcmp(de->d_name, "pg_replslot") == 0) + continue; + + snprintf(childabs, sizeof(childabs), "%s/%s", abspath, de->d_name); + + if (relpath[0] == '\0') + snprintf(childrel, sizeof(childrel), "%s", de->d_name); + else + snprintf(childrel, sizeof(childrel), "%s/%s", relpath, de->d_name); + + /* + * A symlink (e.g. pg_tblspc/) is captured as (linkpath, + * target) so replay can recreate it; we do not recurse through it + * (its contents arrive as RELFILE images via smgr once the symlink + * exists). + */ + if (lstat(childabs, &st) == 0 && S_ISLNK(st.st_mode)) + { + char target[MAXPGPATH]; + ssize_t tlen; + + tlen = readlink(childabs, target, sizeof(target) - 1); + if (tlen < 0 || tlen >= (ssize_t) sizeof(target)) + continue; /* unreadable / too long -- skip */ + target[tlen] = '\0'; + + appendBinaryStringInfo(sbuf, childrel, strlen(childrel) + 1); + appendBinaryStringInfo(sbuf, target, strlen(target) + 1); + (*nsymlinks)++; + continue; + } + + if (lstat(childabs, &st) != 0 || !S_ISDIR(st.st_mode)) + continue; /* only directories and symlinks; skip files */ + + /* append this directory, then recurse into it (parent-before-child) */ + appendBinaryStringInfo(dbuf, childrel, strlen(childrel) + 1); + (*ndirs)++; + + CollectUpgradeDirs(childabs, childrel, dbuf, ndirs, sbuf, nsymlinks); + } + FreeDir(dir); +} + +/* + * Emit one XLOG_UPGRADE_DIRTREE record capturing the after-image of the new + * cluster's directory tree (every subdirectory under PGDATA except pg_wal). + * initdb creates this tree outside the server, so it is not otherwise + * WAL-logged; this lets recovery rebuild the skeleton from WAL without initdb. + * + * Emitted right after XLOG_UPGRADE_START and before the file-image records, so + * the directories exist before any relfile/SLRU image is replayed into them. + */ +XLogRecPtr +XLogWriteUpgradeDirSkel(void) +{ + xl_upgrade_dirtree xlrec; + StringInfoData dbuf; /* directory paths */ + StringInfoData sbuf; /* symlink entries */ + XLogRecPtr RecPtr; + + /* zero padding so no uninitialized stack bytes leak into WAL */ + memset(&xlrec, 0, sizeof(xlrec)); + initStringInfo(&dbuf); + initStringInfo(&sbuf); + xlrec.ndirs = 0; + xlrec.nsymlinks = 0; + CollectUpgradeDirs(DataDir, "", &dbuf, &xlrec.ndirs, + &sbuf, &xlrec.nsymlinks); + xlrec.dir_bytes = (uint32) dbuf.len; + xlrec.sym_bytes = (uint32) sbuf.len; + + XLogBeginInsert(); + XLogRegisterData(&xlrec, SizeOfXLUpgradeDirtree); + if (dbuf.len > 0) + XLogRegisterData(dbuf.data, dbuf.len); + if (sbuf.len > 0) + XLogRegisterData(sbuf.data, sbuf.len); + RecPtr = XLogInsert(RM_PG_UPGRADE_ID, XLOG_UPGRADE_DIRTREE); + + ereport(LOG, + (errmsg("pg_upgrade directory after-image recorded at %X/%X (%u directories, %u symlinks)", + LSN_FORMAT_ARGS(RecPtr), xlrec.ndirs, xlrec.nsymlinks))); + + pfree(dbuf.data); + pfree(sbuf.data); + return RecPtr; +} + +/* + * Emit XLOG_UPGRADE_SLRU_DATA records for all segment files in one SLRU + * directory (pg_xact, pg_multixact/offsets, or pg_multixact/members). These + * FPIs are self-contained: redo is the sole writer of these pages, so it just + * writes the raw bytes back per segment file. + * + * Segments are sorted and packed into records covering contiguous runs + * [first_seg, last_seg] to minimise record count. Returns the LSN of the last + * record, or InvalidXLogRecPtr if the directory is empty. + */ +XLogRecPtr +XLogWriteUpgradeSlruData(uint8 slru_type) +{ + const char *slru_dirs[] = UPGRADE_SLRU_DIRS; + const char *slru_dir; + DIR *dir; + struct dirent *de; + XLogRecPtr lsn = InvalidXLogRecPtr; + + /* sorted list of (seg_number, filepath) pairs */ + typedef struct + { + int64 segno; + char path[MAXPGPATH]; + } SlruSegInfo; + + SlruSegInfo *segs = NULL; + int nsegs = 0; + int segs_alloc = 0; + int i; + + /* + * Max segments per WAL record. Reserve headroom for the + * xl_upgrade_slru_data header and the XLogRecord header so a full batch's + * data plus those headers still fits under XLogRecordMaxSize; otherwise a + * contiguous run of exactly XLogRecordMaxSize/seg_size segments would + * produce a record that XLogInsert() rejects as oversized. (Mirrors the + * relfile batch cap's headroom.) + */ + Size seg_size = SLRU_PAGES_PER_SEGMENT * BLCKSZ; + int max_segs_per_rec = + (int) ((XLogRecordMaxSize - SizeOfXLUpgradeSlruData - SizeOfXLogRecord) + / seg_size); + + if (slru_type >= lengthof(slru_dirs)) + elog(ERROR, "invalid slru_type %u", slru_type); + + slru_dir = slru_dirs[slru_type]; + + /* --- Pass 1: collect and sort segment numbers --- */ + dir = AllocateDir(slru_dir); + if (dir == NULL) + return InvalidXLogRecPtr; + + while ((de = ReadDir(dir, slru_dir)) != NULL) + { + int64 segno; + size_t len = strlen(de->d_name); + + if (de->d_name[0] == '.') + continue; + + /* + * Parse the FULL hex segment name, mirroring SlruScanDirectory() in + * slru.c. A capped read (e.g. "%04") would truncate long (15-digit) + * or high-count (5-6 digit) names to a wrong segment number and + * install the pages at the wrong offset. Accept only all-hex names. + */ + if (strspn(de->d_name, "0123456789ABCDEF") != len) + continue; + segno = strtoi64(de->d_name, NULL, 16); + + if (nsegs >= segs_alloc) + { + segs_alloc = (segs_alloc == 0) ? 64 : segs_alloc * 2; + if (segs == NULL) + segs = palloc_array(SlruSegInfo, segs_alloc); + else + segs = repalloc_array(segs, SlruSegInfo, segs_alloc); + } + segs[nsegs].segno = segno; + snprintf(segs[nsegs].path, MAXPGPATH, "%s/%s", slru_dir, de->d_name); + nsegs++; + } + FreeDir(dir); + + if (nsegs == 0) + { + if (segs) + pfree(segs); + return InvalidXLogRecPtr; + } + + /* sort ascending by segment number */ + { + int k, + n = nsegs; + + /* simple insertion sort -- segment count is typically small */ + for (k = 1; k < n; k++) + { + SlruSegInfo tmp = segs[k]; + int m = k; + + while (m > 0 && segs[m - 1].segno > tmp.segno) + { + segs[m] = segs[m - 1]; + m--; + } + segs[m] = tmp; + } + } + + /* --- Pass 2: pack consecutive segments per record, allocate exact size --- */ + i = 0; + while (i < nsegs) + { + xl_upgrade_slru_data xlrec; + int j; + int batch_count; + Size batch_bytes; + char *packbuf; + + /* Determine how many consecutive segments go in this batch */ + j = i; + while (j < nsegs && + (j - i) < max_segs_per_rec && + (j == i || segs[j].segno == segs[j - 1].segno + 1)) + j++; + + batch_count = j - i; + batch_bytes = (Size) batch_count * seg_size; + + /* Allocate exactly the batch size, not a worst-case buffer. */ + packbuf = palloc(batch_bytes); + + /* Read each segment file into the pack buffer */ + for (int k = 0; k < batch_count; k++) + { + int fd; + ssize_t nbytes; + + char *segbuf = packbuf + (Size) k * seg_size; + Size total = 0; + + fd = OpenTransientFile(segs[i + k].path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open SLRU file \"%s\": %m", + segs[i + k].path))); + + /* + * Read the whole segment. read() may return a short count + * without reaching EOF (e.g. interrupted after a partial + * transfer), so loop: only a return of 0 is genuine EOF. A + * segment that ends before seg_size is a legitimately-partial + * final segment; zero-pad its remainder so redo can stride by + * seg_size. Treating any short read as EOF would zero-fill the + * tail of a full interior segment and corrupt it (for pg_xact, + * silently losing commit status). + */ + for (;;) + { + nbytes = read(fd, segbuf + total, seg_size - total); + if (nbytes < 0) + { + CloseTransientFile(fd); + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read SLRU file \"%s\": %m", + segs[i + k].path))); + } + if (nbytes == 0) + break; /* EOF */ + total += (Size) nbytes; + if (total >= seg_size) + break; + } + CloseTransientFile(fd); + + /* pad a genuinely-short (final) segment to full size */ + if (total < seg_size) + memset(segbuf + total, 0, seg_size - total); + } + + /* zero padding so no uninitialized stack bytes leak into WAL */ + memset(&xlrec, 0, sizeof(xlrec)); + xlrec.slru_type = slru_type; + xlrec.first_seg = segs[i].segno; + xlrec.last_seg = segs[i + batch_count - 1].segno; + xlrec.total_bytes = (uint32) batch_bytes; + +#ifdef USE_ASSERT_CHECKING + + /* + * Fault-injection hook: inflate total_bytes past the payload actually + * registered below, producing a valid-CRC but internally inconsistent + * record that exercises pg_upgrade_redo's SLRU bounds check. Gated + * on USE_ASSERT_CHECKING so a production build never emits such a + * record. + */ + if (getenv("PG_UPGRADE_TEST_CORRUPT_SLRU_LEN") != NULL) + xlrec.total_bytes = (uint32) (batch_bytes + seg_size); +#endif + + XLogBeginInsert(); + XLogRegisterData(&xlrec, SizeOfXLUpgradeSlruData); + XLogRegisterData(packbuf, batch_bytes); + lsn = XLogInsert(RM_PG_UPGRADE_ID, XLOG_UPGRADE_SLRU_DATA); + + pfree(packbuf); + i = j; + } + + pfree(segs); + return lsn; +} + +/* + * Batched emission of XLOG_UPGRADE_RELFILE_DATA. These FPIs are + * self-contained: redo is the sole writer of these relation-file pages. Many + * chunks are packed into one record to use the coarsest WAL granularity: + * + * UpgradeRelfileBatch b; + * XLogUpgradeRelfileBatchBegin(&b); + * for each file: XLogUpgradeRelfileBatchAddFile(&b, path, ...); + * XLogUpgradeRelfileBatchEnd(&b); + * + * The batch is capped a little below XLogRecordMaxSize to leave room for the + * XLogRecord header; a single chunk never exceeds cap minus one entry header, + * so after a flush any chunk fits. + */ +#define UPGRADE_RELFILE_BATCH_CAP \ + ((Size) ((XLogRecordMaxSize / BLCKSZ - 2) * (Size) BLCKSZ)) +#define UPGRADE_RELFILE_MAX_CHUNK \ + ((Size) (((UPGRADE_RELFILE_BATCH_CAP - SizeOfXLUpgradeRelfileEntry) / BLCKSZ) * (Size) BLCKSZ)) + +/* + * Flush the accumulated batch as one WAL record of type "info" + * (XLOG_UPGRADE_RELFILE_DATA for the relfile batch, XLOG_UPGRADE_RELINK for the + * relink manifest -- both accumulate into UpgradeRelfileBatch the same way). + */ +static void +XLogUpgradeBatchFlush(UpgradeRelfileBatch *b, uint8 info) +{ + if (b->used == 0) + return; + + XLogBeginInsert(); + XLogRegisterData(b->buf, b->used); + XLogInsert(RM_PG_UPGRADE_ID, info); + + b->nrecords++; + b->used = 0; + b->nentries = 0; +} + +void +XLogUpgradeRelfileBatchBegin(UpgradeRelfileBatch *b) +{ + b->buf = palloc(UPGRADE_RELFILE_BATCH_CAP); + b->cap = UPGRADE_RELFILE_BATCH_CAP; + b->used = 0; + b->nentries = 0; + b->nrecords = 0; + b->nfiles = 0; +} + +/* + * Add every chunk of one relation file segment to the batch, flushing full + * records as we go. A missing or empty file is silently skipped. + */ +void +XLogUpgradeRelfileBatchAddFile(UpgradeRelfileBatch *b, const char *path, + Oid tsoid, Oid dboid, RelFileNumber rfnum, + uint8 forknum, uint32 segno) +{ + struct stat stbuf; + int fd; + Size filesize; + Size off; + + if (stat(path, &stbuf) != 0) + { + if (errno == ENOENT) + return; + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not stat \"%s\": %m", path))); + } + + filesize = (Size) stbuf.st_size; + + /* + * An empty (0-byte) relation file still needs a record so replay + * recreates it; otherwise the first write to an empty system catalog + * fails with "could not open file". Emit a zero-page entry (nbytes=0); + * RELFILE redo creates the file via smgr without writing any page. Only + * the base segment (segno 0) needs this; higher segments are never empty. + */ + if (filesize == 0) + { + xl_upgrade_relfile_entry ent; + + if (segno != 0) + return; /* an empty non-base segment: nothing to + * create */ + + if (b->used + SizeOfXLUpgradeRelfileEntry > b->cap) + XLogUpgradeBatchFlush(b, XLOG_UPGRADE_RELFILE_DATA); + + memset(&ent, 0, sizeof(ent)); + ent.tablespace_oid = tsoid; + ent.database_oid = dboid; + ent.relfilenumber = rfnum; + ent.forknum = forknum; + ent.segno = segno; + ent.blockoff = 0; + ent.nbytes = 0; /* signals "create empty file, no pages" */ + + memcpy(b->buf + b->used, &ent, SizeOfXLUpgradeRelfileEntry); + b->used += SizeOfXLUpgradeRelfileEntry; + b->nentries++; + b->nfiles++; + return; + } + + fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open \"%s\": %m", path))); + + for (off = 0; off < filesize; off += UPGRADE_RELFILE_MAX_CHUNK) + { + Size chunk = Min(UPGRADE_RELFILE_MAX_CHUNK, filesize - off); + xl_upgrade_relfile_entry ent; + ssize_t nread; + + /* Start a fresh record if this entry would overflow the batch. */ + if (b->used + SizeOfXLUpgradeRelfileEntry + chunk > b->cap) + XLogUpgradeBatchFlush(b, XLOG_UPGRADE_RELFILE_DATA); + + /* zero the struct so its alignment padding is never uninitialized WAL */ + memset(&ent, 0, sizeof(ent)); + ent.tablespace_oid = tsoid; + ent.database_oid = dboid; + ent.relfilenumber = rfnum; + ent.forknum = forknum; + ent.segno = segno; + ent.blockoff = (uint32) (off / BLCKSZ); + ent.nbytes = (uint32) chunk; + + memcpy(b->buf + b->used, &ent, SizeOfXLUpgradeRelfileEntry); + b->used += SizeOfXLUpgradeRelfileEntry; + + nread = pg_pread(fd, b->buf + b->used, chunk, off); + if (nread != (ssize_t) chunk) + { + CloseTransientFile(fd); + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read \"%s\": %m", path))); + } + b->used += chunk; + b->nentries++; + } + + CloseTransientFile(fd); + b->nfiles++; +} + +void +XLogUpgradeRelfileBatchEnd(UpgradeRelfileBatch *b) +{ + XLogUpgradeBatchFlush(b, XLOG_UPGRADE_RELFILE_DATA); + pfree(b->buf); + b->buf = NULL; + + ereport(LOG, + (errmsg("pg_upgrade captured %d relation files in %d WAL record(s)", + b->nfiles, b->nrecords))); +} + +/* + * Batched emission of XLOG_UPGRADE_RELINK -- the manifest of user relation + * files the window omits. Each entry is a fixed-size identity; no file data. + * Redo links each from the standby's old datadir into the new skeleton. + * + * Uses the same payload cap as the relfile batch (UPGRADE_RELFILE_BATCH_CAP): + * both accumulate into the same WAL-record payload limit. + */ +void +XLogUpgradeRelinkBatchBegin(UpgradeRelfileBatch *b, uint8 xfer_mode) +{ + b->buf = palloc(UPGRADE_RELFILE_BATCH_CAP); + b->cap = UPGRADE_RELFILE_BATCH_CAP; + b->used = 0; + b->nentries = 0; + b->nrecords = 0; + b->nfiles = 0; + b->xfer_mode = xfer_mode; +} + +void +XLogUpgradeRelinkBatchAdd(UpgradeRelfileBatch *b, Oid tsoid, Oid dboid, + RelFileNumber rfnum, uint8 forknum, uint32 segno) +{ + xl_upgrade_relink_entry ent; + + if (b->used + SizeOfXLUpgradeRelinkEntry > b->cap) + XLogUpgradeBatchFlush(b, XLOG_UPGRADE_RELINK); + + /* zero the struct so alignment padding is never uninitialized WAL */ + memset(&ent, 0, sizeof(ent)); + ent.tablespace_oid = tsoid; + ent.database_oid = dboid; + ent.relfilenumber = rfnum; + ent.forknum = forknum; + ent.transfer_mode = b->xfer_mode; + ent.segno = segno; + + memcpy(b->buf + b->used, &ent, SizeOfXLUpgradeRelinkEntry); + b->used += SizeOfXLUpgradeRelinkEntry; + b->nentries++; + b->nfiles++; +} + +void +XLogUpgradeRelinkBatchEnd(UpgradeRelfileBatch *b) +{ + XLogUpgradeBatchFlush(b, XLOG_UPGRADE_RELINK); + pfree(b->buf); + b->buf = NULL; + + ereport(LOG, + (errmsg("pg_upgrade recorded %d user relation files to relink in %d WAL record(s)", + b->nfiles, b->nrecords))); +} + +/* + * Emit XLOG_UPGRADE_RAWFILE -- a verbatim image of a non-relation file + * (pg_filenode.map, PG_VERSION) so the cluster can be rebuilt from an empty + * data directory. "path" is the PGDATA-relative path. Returns the record LSN, + * or InvalidXLogRecPtr if the file is absent or empty. + */ +XLogRecPtr +XLogWriteUpgradeRawFile(const char *path) +{ + xl_upgrade_rawfile xlrec; + struct stat stbuf; + int fd; + char *buf; + ssize_t nbytes; + uint32 path_len = (uint32) strlen(path); + XLogRecPtr lsn; + + if (stat(path, &stbuf) != 0) + { + if (errno == ENOENT) + return InvalidXLogRecPtr; + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not stat \"%s\": %m", path))); + } + + if (stbuf.st_size == 0) + return InvalidXLogRecPtr; + + if ((Size) stbuf.st_size + path_len + SizeOfXLUpgradeRawfile > XLogRecordMaxSize) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("file \"%s\" is too large to capture in a pg_upgrade WAL record", + path))); + + buf = palloc(stbuf.st_size); + + fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open \"%s\": %m", path))); + nbytes = read(fd, buf, stbuf.st_size); + CloseTransientFile(fd); + if (nbytes != stbuf.st_size) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read \"%s\": %m", path))); + + /* zero padding so no uninitialized stack bytes leak into WAL */ + memset(&xlrec, 0, sizeof(xlrec)); + xlrec.path_len = path_len; + xlrec.data_len = (uint32) nbytes; + + XLogBeginInsert(); + XLogRegisterData(&xlrec, SizeOfXLUpgradeRawfile); + XLogRegisterData(unconstify(char *, path), path_len); + XLogRegisterData(buf, nbytes); + lsn = XLogInsert(RM_PG_UPGRADE_ID, XLOG_UPGRADE_RAWFILE); + + pfree(buf); + return lsn; +} + +/* + * Flush all SLRU (CLOG, commit-ts, multixact) dirty pages to disk and fsync + * their segment files, so the SLRU_DATA images captured by the upgrade window + * read the final on-disk state. pg_upgrade runs the burst server with the + * SLRU-related pages possibly still dirty in the buffers, and (on some paths) + * with dirty OS-cache writes not yet synced, so: + * + * - CheckPointCLOG/CheckPointCommitTs/CheckPointMultiXact write out the + * dirty SLRU buffers to the segment files, and + * - the fsync loop below durably syncs those segment files and their parent + * directories. + * + * This does not request a checkpoint. CN (the recovery anchor) is the + * DB_SHUTDOWNED checkpoint pg_resetwal wrote before the burst server started + * and already precedes XLOG_UPGRADE_START; a checkpoint record here would + * displace it as the last checkpoint before START. So the SLRUs are flushed + * directly rather than through RequestCheckpoint(). + */ +void +XLogFlushUpgradeSLRU(void) +{ + /* + * the same SLRU flush steps CheckPointGuts() performs, minus the + * checkpoint + */ + CheckPointCLOG(); + CheckPointCommitTs(); + CheckPointMultiXact(); + + /* + * Fsync all SLRU segment files and their parent directories to catch + * evicted pages whose OS-cache writes were not synced under fsync=off. + */ + { + static const char *const slru_dirs[] = UPGRADE_SLRU_DIRS; + int i; + + for (i = 0; i < lengthof(slru_dirs); i++) + { + DIR *dir; + struct dirent *de; + + dir = AllocateDir(slru_dirs[i]); + if (dir == NULL) + continue; + + while ((de = ReadDir(dir, slru_dirs[i])) != NULL) + { + char path[MAXPGPATH]; + + if (de->d_name[0] == '.') + continue; + + /* + * SLRU segment names are short UPPERCASE hex strings (see + * SlruFileName / SlruScanDirectory), not WAL names. + */ + if (strlen(de->d_name) > 8) + continue; + if (strspn(de->d_name, "0123456789ABCDEF") != strlen(de->d_name)) + continue; + + snprintf(path, sizeof(path), "%s/%s", + slru_dirs[i], de->d_name); + + /* fsync_fname ignores enableFsync; forces actual kernel sync */ + (void) fsync_fname(path, false); + } + FreeDir(dir); + + /* also fsync the directory itself */ + (void) fsync_fname(slru_dirs[i], true); + } + } +} + /* * Check if any of the GUC parameters that are critical for hot standby * have changed, and update the value in pg_control file if necessary. diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c index 65bbaeda59..1e43317a3d 100644 --- a/src/backend/access/transam/xlogfuncs.c +++ b/src/backend/access/transam/xlogfuncs.c @@ -19,14 +19,21 @@ #include #include "access/htup_details.h" +#include "access/pgupgrade_wal.h" /* XLogFlushUpgradeSLRU, emit fns */ +#include "access/transam.h" /* FirstNormalObjectId */ #include "access/xlog_internal.h" #include "access/xlogbackup.h" #include "access/xlogrecovery.h" #include "catalog/pg_authid.h" +#include "catalog/pg_largeobject_d.h" /* LargeObject*Id (user-data catalogs) */ +#include "catalog/pg_largeobject_metadata_d.h" +#include "catalog/pg_tablespace_d.h" /* DEFAULTTABLESPACE_OID */ #include "catalog/pg_type.h" +#include "common/relpath.h" /* PG_TBLSPC_DIR, TABLESPACE_VERSION_DIRECTORY */ #include "funcapi.h" #include "miscadmin.h" #include "pgstat.h" +#include "replication/slot.h" #include "utils/acl.h" #include "replication/walreceiver.h" #include "storage/fd.h" @@ -300,6 +307,464 @@ pg_create_restore_point(PG_FUNCTION_ARGS) * archiving process. Note that the data before this point is written out * to the kernel, but is not necessarily synced to disk. */ + +/* + * pg_upgrade --wal-upgrade window emission. + * + * The individual START/COMPLETE/DIRTREE/SLRU/relfile steps are driven entirely + * from C by EmitUpgradeWalWindow() (below), reachable only through the single + * binary-upgrade-gated SQL entry point binary_upgrade_emit_wal_window() -- so + * these WAL-injection primitives are not exposed as reusable SQL on a normal + * cluster. The static helpers (parse_relfile_name, + * is_transferred_user_data_catalog, capture_dir_files, capture_all_relfiles) + * and EmitUpgradeWalWindow() follow. + */ + +/* + * Parse a relation file name "[_fork][.segno]" into its parts. + * Returns true if it is a relation file, filling *rfnum, *forknum (0=main, + * 1=fsm, 2=vm, 3=init) and *segno. Returns false for non-relation files + * (PG_VERSION, pg_filenode.map, pg_internal.init, etc.). + */ +static bool +parse_relfile_name(const char *name, RelFileNumber *rfnum, + uint8 *forknum, uint32 *segno) +{ + char base[MAXPGPATH]; + char *p; + unsigned long n; + char *endp; + + if (name[0] < '1' || name[0] > '9') + return false; /* relation files start with a nonzero digit */ + + strlcpy(base, name, sizeof(base)); + + *segno = 0; + p = strrchr(base, '.'); + if (p != NULL && p[1] >= '0' && p[1] <= '9') + { + *segno = (uint32) strtoul(p + 1, NULL, 10); + *p = '\0'; + } + + *forknum = 0; + if ((p = strstr(base, "_vm")) != NULL && p[3] == '\0') + { + *forknum = 2; + *p = '\0'; + } + else if ((p = strstr(base, "_fsm")) != NULL && p[4] == '\0') + { + *forknum = 1; + *p = '\0'; + } + else if ((p = strstr(base, "_init")) != NULL && p[5] == '\0') + { + *forknum = 3; + *p = '\0'; + } + + errno = 0; + n = strtoul(base, &endp, 10); + if (errno != 0 || *endp != '\0' || n == 0 || n > PG_UINT32_MAX) + return false; + *rfnum = (RelFileNumber) n; + return true; +} + +/* + * Is this relfilenumber one of the pinned system catalogs that pg_upgrade + * nonetheless transfers verbatim from the old cluster (rather than + * regenerating), so its file is already on disk in the new cluster and does + * not travel through the upgrade window? + * + * pg_upgrade treats pg_largeobject and pg_largeobject_metadata as "user data": + * they hold large-object contents/ownership that pg_dump does not emit, so it + * copies/links their files like ordinary user relations (see + * get_rel_infos_query() in src/bin/pg_upgrade/info.c). pg_largeobject is + * handled this way for every source version; pg_largeobject_metadata only for + * old clusters >= v16 (before v16 the aclitem on-disk format differed, so + * pg_upgrade regenerates it via dump/restore -- in which case it IS a fresh + * system catalog that the window must carry). + * + * These have fixed relfilenumbers (== their pinned catalog/index OIDs) unless + * pg_upgrade rewrote them, in which case the relfilenumber is >= 16384 and the + * ordinary user-relation test already excludes them. So matching the pinned + * numbers here is exact. + */ +static bool +is_transferred_user_data_catalog(RelFileNumber rfnum, uint32 old_major) +{ + if (rfnum == LargeObjectRelationId || rfnum == LargeObjectLOidPNIndexId) + return true; + if (old_major >= 160000 && + (rfnum == LargeObjectMetadataRelationId || + rfnum == LargeObjectMetadataOidIndexId)) + return true; + return false; +} + +/* + * Capture every file in one PGDATA subdirectory (base/ or global) + * as WAL. Relation files -> XLOG_UPGRADE_RELFILE_DATA (replayed through the + * buffer manager); pg_filenode.map and PG_VERSION -> XLOG_UPGRADE_RAWFILE + * (written verbatim, since they are not buffered relations). Everything else + * (pg_internal.init and friends) is regenerated by the server and skipped. + */ +static void +capture_dir_files(UpgradeRelfileBatch *batch, UpgradeRelfileBatch *relink, + const char *reldir, Oid tsoid, Oid dboid, uint32 old_major) +{ + DIR *dir; + struct dirent *de; + char path[MAXPGPATH]; + + /* + * Unlogged relations (identified by an _init fork) must not have their + * main/fsm/vm forks captured: end-of-recovery ResetUnloggedRelations() + * rebuilds the main fork from _init with O_CREAT|O_EXCL and would FATAL + * if RELFILE redo had already recreated it. Capture only the _init fork. + * + * Pass 1: collect the set of relfilenumbers that have an _init fork. + */ + RelFileNumber *unlogged = NULL; + int n_unlogged = 0; + int unlogged_alloc = 0; + + dir = AllocateDir(reldir); + if (dir == NULL) + return; + + while ((de = ReadDir(dir, reldir)) != NULL) + { + RelFileNumber rfnum; + uint8 forknum; + uint32 segno; + + if (parse_relfile_name(de->d_name, &rfnum, &forknum, &segno) && + forknum == INIT_FORKNUM) + { + if (n_unlogged >= unlogged_alloc) + { + unlogged_alloc = (unlogged_alloc == 0) ? 16 : unlogged_alloc * 2; + if (unlogged == NULL) + unlogged = palloc_array(RelFileNumber, unlogged_alloc); + else + unlogged = repalloc_array(unlogged, RelFileNumber, unlogged_alloc); + } + unlogged[n_unlogged++] = rfnum; + } + } + FreeDir(dir); + + /* Pass 2: capture files, skipping the main/fsm/vm forks of unlogged rels. */ + dir = AllocateDir(reldir); + if (dir == NULL) + { + if (unlogged != NULL) + pfree(unlogged); + return; + } + + while ((de = ReadDir(dir, reldir)) != NULL) + { + RelFileNumber rfnum; + uint8 forknum; + uint32 segno; + + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + + snprintf(path, sizeof(path), "%s/%s", reldir, de->d_name); + + if (parse_relfile_name(de->d_name, &rfnum, &forknum, &segno)) + { + /* + * Only system relations travel in the window. User relations + * (relfilenode >= FirstNormalObjectId) are linked/copied verbatim + * from the old datadir into the new one by pg_upgrade's transfer + * step, so their contents are already present on disk and do not + * need to travel through WAL. Logging them would make the + * upgrade window scale with total user-data size (a full backup + * dumped into WAL); skipping them makes it scale with the schema + * instead. Only pg_upgrade-rewritten system catalogs, SLRU, and + * the directory skeleton are logged. + * + * A streaming standby has no transfer step, so instead of the + * data the file's identity is recorded in the RELINK manifest: on + * redo the standby links it from its own retained old datadir + * into the new skeleton (the same old->new link the primary just + * did). The manifest entry is emitted from the same branch that + * skips the file, so the manifest lists exactly the files the + * window omits. + */ + if (rfnum >= FirstNormalObjectId) + { + XLogUpgradeRelinkBatchAdd(relink, tsoid, dboid, rfnum, + forknum, segno); + continue; + } + + /* + * A few pinned system catalogs (pg_largeobject and, for old + * clusters >= v16, pg_largeobject_metadata) are likewise + * transferred verbatim by pg_upgrade rather than regenerated -- + * they carry user large-object data. They are omitted from the + * window and relinked exactly like ordinary user relations. + */ + if (is_transferred_user_data_catalog(rfnum, old_major)) + { + XLogUpgradeRelinkBatchAdd(relink, tsoid, dboid, rfnum, + forknum, segno); + continue; + } + + /* Skip main/fsm/vm forks of unlogged relations (see comment). */ + if (forknum != INIT_FORKNUM) + { + bool is_unlogged = false; + + for (int i = 0; i < n_unlogged; i++) + { + if (unlogged[i] == rfnum) + { + is_unlogged = true; + break; + } + } + if (is_unlogged) + continue; + } + + XLogUpgradeRelfileBatchAddFile(batch, path, tsoid, dboid, rfnum, + forknum, segno); + } + else if (strcmp(de->d_name, "pg_filenode.map") == 0 || + strcmp(de->d_name, "PG_VERSION") == 0) + (void) XLogWriteUpgradeRawFile(path); + /* else: pg_internal.init, etc. -- regenerated, skip */ + } + FreeDir(dir); + + if (unlogged != NULL) + pfree(unlogged); +} + +/* + * capture_all_relfiles() + * + * Capture the full physical image of the cluster as WAL: every relation file + * under base// and global/, plus raw pg_filenode.map / PG_VERSION, so + * first-startup replay can reconstruct the cluster from an empty data + * directory. Recovery is anchored at CN, so these images are the sole source + * of the catalogs; a preceding CHECKPOINT makes the files we read final. + * + * Internal C helper driven by EmitUpgradeWalWindow(); not SQL-callable. + */ +static void +capture_all_relfiles(uint32 old_major, uint8 transfer_mode) +{ + UpgradeRelfileBatch batch; + UpgradeRelfileBatch relink; + DIR *basedir; + struct dirent *de; + + XLogUpgradeRelfileBatchBegin(&batch); + XLogUpgradeRelinkBatchBegin(&relink, transfer_mode); + + /* + * Shared catalogs live in global/ (tablespace GLOBALTABLESPACE_OID, db + * 0). + */ + capture_dir_files(&batch, &relink, "global", GLOBALTABLESPACE_OID, + InvalidOid, old_major); + + /* Every database directory under base/. */ + basedir = AllocateDir("base"); + if (basedir != NULL) + { + while ((de = ReadDir(basedir, "base")) != NULL) + { + char dbpath[MAXPGPATH]; + char *endp; + unsigned long dboid; + + if (de->d_name[0] < '1' || de->d_name[0] > '9') + continue; + errno = 0; + dboid = strtoul(de->d_name, &endp, 10); + if (errno != 0 || *endp != '\0') + continue; + + snprintf(dbpath, sizeof(dbpath), "base/%s", de->d_name); + capture_dir_files(&batch, &relink, dbpath, DEFAULTTABLESPACE_OID, + (Oid) dboid, old_major); + } + FreeDir(basedir); + } + + /* + * Relations in user-defined tablespaces. Each pg_tblspc/ + * symlinks to TABLESPACE_VERSION_DIRECTORY//; walk each and + * capture the relfiles with tsoid= so RELFILE redo (resolving + * spcOid via the symlink) places them correctly. + */ + { + DIR *tsdir = AllocateDir(PG_TBLSPC_DIR); + + if (tsdir != NULL) + { + struct dirent *tde; + + while ((tde = ReadDir(tsdir, PG_TBLSPC_DIR)) != NULL) + { + char verpath[MAXPGPATH]; + char *endp; + unsigned long spcoid; + DIR *verdir; + struct dirent *vde; + + if (tde->d_name[0] < '1' || tde->d_name[0] > '9') + continue; + errno = 0; + spcoid = strtoul(tde->d_name, &endp, 10); + if (errno != 0 || *endp != '\0') + continue; + + /* pg_tblspc/// */ + snprintf(verpath, sizeof(verpath), "%s/%s/%s", + PG_TBLSPC_DIR, tde->d_name, + TABLESPACE_VERSION_DIRECTORY); + + verdir = AllocateDir(verpath); + if (verdir == NULL) + continue; + + while ((vde = ReadDir(verdir, verpath)) != NULL) + { + char dbpath[MAXPGPATH]; + char *dendp; + unsigned long dboid; + + if (vde->d_name[0] < '1' || vde->d_name[0] > '9') + continue; + errno = 0; + dboid = strtoul(vde->d_name, &dendp, 10); + if (errno != 0 || *dendp != '\0') + continue; + + snprintf(dbpath, sizeof(dbpath), "%s/%s", + verpath, vde->d_name); + capture_dir_files(&batch, &relink, dbpath, (Oid) spcoid, + (Oid) dboid, old_major); + } + FreeDir(verdir); + } + FreeDir(tsdir); + } + } + + /* Flush the last partial record of relation-file images. */ + XLogUpgradeRelfileBatchEnd(&batch); + + /* Flush the manifest of user relations for the standby to relink. */ + XLogUpgradeRelinkBatchEnd(&relink); + + /* + * Top-level PG_VERSION (needed pre-recovery, but capture for + * completeness). + */ + (void) XLogWriteUpgradeRawFile("PG_VERSION"); +} + +/* + * EmitUpgradeWalWindow() + * + * Emit the entire pg_upgrade --wal-upgrade window as WAL, in one C routine, so + * the frontend drives the whole capture with a single gated backend call rather + * than a sequence of individually SQL-exposed WAL-injection primitives. The + * emission order matters and is: + * + * 0. create the retention slot pinned at CN (restart_lsn = CN's LSN) so the + * window CN..COMPLETE cannot be recycled before a standby streams it. + * 1. flush SLRU dirty pages to disk (no checkpoint). CN -- the recovery + * anchor, the last checkpoint preceding XLOG_UPGRADE_START -- is not taken + * here: it is the DB_SHUTDOWNED checkpoint pg_resetwal wrote at the start + * of the nextxlogfile segment before the burst server started, and it + * already precedes START. A checkpoint here would displace it. + * 2. XLOG_UPGRADE_START (carries the new PG_VERSION string). + * 3. XLOG_UPGRADE_DIRTREE (directory skeleton, before any file image). + * 4. relation-file images (system rels) + the RELINK manifest (user rels). + * 5. XLOG_UPGRADE_SLRU_DATA for pg_xact, multixact offsets, multixact members. + * 6. XLOG_UPGRADE_COMPLETE (terminal marker) + set the durable pg_control + * upgrade_finalized flag -- unless skip_complete is set (assert-build test + * hook simulating a crash mid-window). + * + * Runs only in binary-upgrade mode (the caller enforces IsBinaryUpgrade); the + * burst server is started with -b, so all of pg_upgrade's WAL generation stays + * behind that gate and is never exposed as reusable SQL on a normal cluster. + */ +void +EmitUpgradeWalWindow(uint32 old_major, uint32 new_major, uint8 transfer_mode, + bool skip_complete) +{ + if (RecoveryInProgress()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is in progress"))); + + /* + * The upgrade window (CN..COMPLETE) is pinned in pg_wal/ against + * recycling by a migrated physical replication slot, created by + * pg_upgrade on this same burst server just before this call (see + * pg_upgrade.c). That slot's restart_lsn is GetRedoRecPtr() at creation + * time, which -- because the burst server started from the reset-written + * CN shutdown checkpoint and has written no checkpoint since -- is + * exactly CN, so it pins the whole window size-independently (server.c + * sets max_slot_wal_keep_size=-1). A standby later connects with + * primary_slot_name = that slot to pull the window. With no migrated slot + * there is no streaming consumer and the window need not be pinned + * (recycled as ordinary WAL, or delivered via the archive). + * + * Step 1: flush SLRU dirty pages durably (see header for why no + * checkpoint). + */ + XLogFlushUpgradeSLRU(); + + /* + * Mark the control file "upgrade started" durably before emitting START. + * This is the crash-atomicity anchor: if the burst server dies (or, in + * the test hook, suppresses COMPLETE) after this point but before + * finalize, the new cluster's control file carries upgrade_started && + * !upgrade_finalized, which PerformWalUpgradeIfNeeded() refuses to + * auto-serve -- independent of whether the START-bearing WAL survived to + * first boot (see the crash-atomicity guard there). + */ + SetControlFileUpgradeStarted(); + + /* 2. START */ + (void) XLogWritePgUpgrade(true, old_major, new_major); + + /* 3. directory skeleton */ + (void) XLogWriteUpgradeDirSkel(); + + /* 4. relation-file images + RELINK manifest */ + capture_all_relfiles(old_major, transfer_mode); + + /* 5. SLRU bulk images: pg_xact, multixact offsets, multixact members */ + (void) XLogWriteUpgradeSlruData(0); + (void) XLogWriteUpgradeSlruData(1); + (void) XLogWriteUpgradeSlruData(2); + + /* 6. COMPLETE + durable finalized flag (unless suppressed for testing) */ + if (!skip_complete) + { + (void) XLogWritePgUpgrade(false, old_major, new_major); + SetControlFileUpgradeFinalized(); + } +} + Datum pg_current_wal_lsn(PG_FUNCTION_ARGS) { diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index c0ae4d3f63..eae43c3b80 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -101,6 +101,7 @@ char *PrimaryConnInfo = NULL; char *PrimarySlotName = NULL; bool wal_receiver_create_temp_slot = false; + /* * recoveryTargetTimeLineGoal: what the user requested, if any * @@ -302,6 +303,19 @@ static bool backupEndRequired = false; */ bool reachedConsistency = false; +/* + * True while replaying a pg_upgrade --wal-upgrade window. While set, hot + * standby must NOT go active: the cluster is only half-upgraded (new catalogs + * partially applied), so no read-only connection may observe it. Normally set + * by the XLOG_UPGRADE_START redo and cleared by XLOG_UPGRADE_COMPLETE + * (pg_upgrade_redo()). For a streaming standby staged without initdb it is set + * even earlier -- at arm time in PerformWalUpgradeIfNeeded(), before any record + * replays -- because such a skeleton has no shared catalogs on disk until the + * window streams in, so it must not admit connections in the gap before + * XLOG_UPGRADE_START. + */ +bool pgUpgradeReplayInProgress = false; + /* Buffers dedicated to consistency checks of size BLCKSZ */ static char *replay_image_masked = NULL; static char *primary_image_masked = NULL; @@ -1815,6 +1829,23 @@ PerformWalRecovery(void) ereport(FATAL, (errmsg("requested recovery stop point is before consistent recovery point"))); + /* + * Refuse to stop inside a --wal-upgrade window. + * pgUpgradeReplayInProgress is set at the window's + * XLOG_UPGRADE_START and cleared only at XLOG_UPGRADE_COMPLETE, + * so if it is still set here the recovery target landed between + * them: the catalog is only half-upgraded. Promoting (or pausing + * then promoting) would open that half-applied cluster + * read-write. There is no consistent cluster to expose at an + * intra-window point, so fail rather than come up corrupt; the + * operator must choose a target at or after the upgrade. + */ + if (pgUpgradeReplayInProgress) + ereport(FATAL, + (errmsg("requested recovery stop point is inside a pg_upgrade window"), + errdetail("The upgrade has begun but not completed at this point, so the cluster would be only partially upgraded."), + errhint("Choose a recovery target at or after the end of the pg_upgrade window, or remove the recovery target to replay the whole upgrade."))); + /* * This is the last point where we can restart recovery with a new * recovery target, if we shutdown and begin again. After this, @@ -2233,6 +2264,7 @@ CheckRecoveryConsistency(void) if (standbyState == STANDBY_SNAPSHOT_READY && !LocalHotStandbyActive && reachedConsistency && + !pgUpgradeReplayInProgress && IsUnderPostmaster) { SpinLockAcquire(&XLogRecoveryCtl->info_lck); diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 90c7c4528e..fb5bcdeb1b 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -1537,6 +1537,13 @@ checkControlFile(void) fp = AllocateFile(path, PG_BINARY_R); if (fp == NULL) { + /* + * For a --wal-upgrade streaming standby staged without initdb, the + * pg_control was already synthesized earlier by checkDataDir() + * (before its PG_VERSION gate), so it exists by now. If it is still + * missing here this is a genuinely broken data directory -- fail + * loudly as usual. + */ write_stderr("%s: could not find the database system\n" "Expected to find it in the directory \"%s\",\n" "but could not open file \"%s\": %m\n", diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index d91b7caac0..70a598b4a0 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -19,6 +19,7 @@ */ #include "postgres.h" +#include "access/pgupgrade_wal.h" /* PerformWalUpgradeIfNeeded */ #include "access/xlog.h" #include "access/xlogrecovery.h" #include "access/xlogutils.h" @@ -251,6 +252,16 @@ StartupProcessMain(const void *startup_data, size_t startup_data_len) */ sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); + /* + * Detect a pending pg_upgrade --wal-upgrade and, if found, arm the + * control file so the StartupXLOG() below replays the upgrade window (see + * the function for the streaming-standby / local-window / archive-PITR + * paths). A crashed partial upgrade (window began but never reached + * COMPLETE) FATALs here with a re-run instruction rather than serving a + * half-upgraded cluster. + */ + PerformWalUpgradeIfNeeded(); + /* * Do what we came for. */ diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index c931d9b4fa..3af34be400 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -53,6 +53,7 @@ #include "access/timeline.h" #include "access/transam.h" +#include "access/pgupgrade_wal.h" #include "access/twophase.h" #include "access/xact.h" #include "access/xlog_internal.h" diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 9ab282a76d..f6a0ae039e 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -5848,6 +5848,31 @@ MarkBufferDirtyHint(Buffer buffer, bool buffer_std) if (!BufferIsValid(buffer)) elog(ERROR, "bad buffer ID: %d", buffer); + /* + * pg_upgrade --wal-upgrade: suppress hint-bit dirtying on the + * binary-upgrade burst server. + * + * A hint-bit change does not advance the page's pd_lsn, but it DOES mark + * the (permanent) page dirty, so the next checkpoint flushes it and -- + * per the WAL-before-data rule in FlushBuffer() -- forces XLogFlush up to + * the page's existing pd_lsn. Under --wal-upgrade the burst server that + * captures the window runs after pg_resetwal has rewound WAL to CN (the + * old cluster's nextxlogfile), while the restore stamped catalog pages + * with LSNs from BEFORE the rewind (past the current end of WAL). If a + * plain catalog read on that server set a hint bit and dirtied such a + * page, its shutdown checkpoint would try to flush WAL that no longer + * exists and FATAL ("xlog flush request ... is not satisfied"). + * + * Hint bits are a non-critical optimization -- this function already does + * not guarantee the buffer is marked dirty (e.g. on a hot standby), and + * callers tolerate that -- so skipping them in binary-upgrade mode is + * safe: the new cluster simply re-derives them on first access. This + * keeps the captured pages clean so the burst server shuts down cleanly + * with WAL rewound to CN. + */ + if (IsBinaryUpgrade) + return; + if (BufferIsLocal(buffer)) { MarkLocalBufferDirty(buffer); diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c index b505a6b4fe..accb565c42 100644 --- a/src/backend/utils/adt/pg_upgrade_support.c +++ b/src/backend/utils/adt/pg_upgrade_support.c @@ -11,6 +11,7 @@ #include "postgres.h" +#include "access/pgupgrade_wal.h" #include "access/relation.h" #include "access/table.h" #include "catalog/binary_upgrade.h" @@ -435,3 +436,32 @@ binary_upgrade_create_conflict_detection_slot(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + +/* + * binary_upgrade_emit_wal_window + * + * pg_upgrade --wal-upgrade: emit the whole upgrade window (CN..COMPLETE) as WAL + * in a single call. The frontend invokes this once, on the burst server it + * started in binary-upgrade mode; the whole capture runs inside this one call + * rather than through separate SQL-exposed WAL-injection primitives. Gated on + * IsBinaryUpgrade so the WAL-generation machinery is never reachable as SQL on + * an ordinary cluster. + * + * Args: old_major_version int4, new_major_version int4, transfer_mode int4, + * skip_complete bool (test-only: omit the COMPLETE marker to simulate a + * crash mid-window). + */ +Datum +binary_upgrade_emit_wal_window(PG_FUNCTION_ARGS) +{ + uint32 old_major = PG_GETARG_UINT32(0); + uint32 new_major = PG_GETARG_UINT32(1); + uint8 transfer_mode = (uint8) PG_GETARG_INT32(2); + bool skip_complete = PG_GETARG_BOOL(3); + + CHECK_IS_BINARY_UPGRADE; + + EmitUpgradeWalWindow(old_major, new_major, transfer_mode, skip_complete); + + PG_RETURN_VOID(); +} diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c index 7ffc808073..61e55a98c6 100644 --- a/src/backend/utils/init/miscinit.c +++ b/src/backend/utils/init/miscinit.c @@ -30,6 +30,7 @@ #include "access/htup_details.h" #include "access/parallel.h" +#include "access/pgupgrade_wal.h" /* UpgradeSignalStaged */ #include "catalog/pg_authid.h" #include "common/file_perm.h" #include "libpq/libpq.h" @@ -378,6 +379,91 @@ checkDataDir(void) data_directory_mode = pg_dir_create_mode; #endif + /* + * --wal-upgrade recovery target without initdb. Before the PG_VERSION / + * pg_control gates, if this data directory is staged for wal-upgrade + * recovery (the pg_upgrade.signal sentinel is present) and lacks a valid + * new-version PG_VERSION, synthesize PG_VERSION + global/pg_control from + * this binary's constants so the pre-start gates pass. The recovery mode + * is inferred from stock config: primary_conninfo set -> stream from the + * live primary; recovery.signal present -> the window arrives via + * restore_command. Gated on the sentinel so an ordinary un-initdb'd + * directory still fails at ValidatePgVersion() below. + */ + { + char recsig[MAXPGPATH]; + char verpath[MAXPGPATH]; + struct stat st; + bool have_upgradesig; + + snprintf(recsig, sizeof(recsig), "%s/%s", DataDir, RECOVERY_SIGNAL_FILE); + snprintf(verpath, sizeof(verpath), "%s/PG_VERSION", DataDir); + have_upgradesig = UpgradeSignalStaged(); + + /* + * Mode inference (keep in lockstep with PerformWalUpgradeIfNeeded()): + * sentinel + primary_conninfo => streaming; sentinel + + * recovery.signal (and no primary) => archive/PITR. The arm in + * PerformWalUpgradeIfNeeded makes the SAME split -- + * ArmFromLocalDerivationIfConfigured() runs the streaming path + * exactly when the sentinel + primary_conninfo are set, and its + * archive arm is reached only when that returned false -- so a dir + * cannot be classified one way here and the other way there. + */ + if (have_upgradesig && + PrimaryConnInfo != NULL && PrimaryConnInfo[0] != '\0') + { + /* + * Streaming standby: a fresh new-version skeleton staged with the + * pg_upgrade.signal sentinel and a primary to stream from. User + * relations are not in the window; they are linked in from the + * retained old datadir (named in pg_upgrade.signal's contents) by + * the RELINK-manifest redo. Two skeleton shapes are accepted: + * + * - a real initdb skeleton (has a valid new-version pg_control / + * PG_VERSION already): it passes the version gate on its own, so + * nothing to synthesize -- the anchor arm just re-stamps its + * control file at CN. - a bare skeleton (no PG_VERSION yet): + * synthesize a minimal new-version pg_control + PG_VERSION so the + * gate passes. + * + * Either way the skeleton must NOT be a populated old datadir; + * that is the relink source named in the sentinel, never the + * streaming target. We cannot verify "new-version skeleton" here + * (ReadControlFile below is what version-checks), but we never + * synthesize OVER an existing control file, so an old datadir + * mistakenly used here fails cleanly at the version gate. + */ + if (stat(verpath, &st) != 0) + SynthesizeUpgradeStreamControlFile(false); + } + else if (have_upgradesig && stat(recsig, &st) == 0) + { + /* + * ARCHIVE-PITR cross-version recovery (Phase 2). The data + * directory here is a pre-upgrade base backup that Phase 1 (the + * OLD binary) replayed up to the upgrade boundary and shut down, + * so it still carries the OLD major's pg_control/PG_VERSION -- + * which this NEW binary would reject at the version gate below. + * This keys off the ordinary recovery.signal (this IS an archive + * restore, and no primary_conninfo, so not the streaming branch + * above) plus the pg_upgrade.signal sentinel dropped alongside + * the staged upgrade window: only a genuine cross-version + * upgrade-PITR has both, so a normal same-version PITR + * (recovery.signal but no sentinel) is never affected. A plain + * stat() matches how the backend already selects recovery mode + * (see readRecoverySignalFile()); the authoritative check that a + * complete window is really present is left to + * PerformWalUpgradeIfNeeded(), which FATALs on a partial window. + * Synthesize a NEW-version pg_control from this binary's + * constants so the gate passes; recovery then re-anchors at CN, + * adopts the window's sysid, and replays the window + archived + * tail. + */ + SynthesizeUpgradeStreamControlFile(true); + } + } + /* Check for PG_VERSION */ ValidatePgVersion(DataDir); } diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index c9118e7198..7ad4e2a3ce 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2361,6 +2361,21 @@ max => 'INT_MAX', }, +{ name => 'pg_upgrade_standby_old_datadir', type => 'string', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the path of this standby\'s retained pre-upgrade data directory for pg_upgrade --wal-upgrade.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'pg_upgrade_standby_old_datadir', + boot_val => '""', +}, + +{ name => 'pg_upgrade_standby_transfer_mode', type => 'enum', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY', + short_desc => 'Sets how a pg_upgrade --wal-upgrade streaming standby places user relation files.', + long_desc => 'The default, mirror, reproduces the transfer mode the upgraded primary used; the others override it on this standby.', + variable => 'pg_upgrade_standby_transfer_mode', + boot_val => 'PG_UPGRADE_XFER_MIRROR', + options => 'pg_upgrade_standby_transfer_mode_options', +}, + { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', short_desc => 'Controls the planner\'s selection of custom or generic plan.', long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better. This can be set to override the default behavior.', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index d7942f50a7..f95517a472 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -383,6 +383,14 @@ #primary_conninfo = '' # connection string to sending server #primary_slot_name = '' # replication slot on sending server +#pg_upgrade_standby_transfer_mode = 'mirror' # how a --wal-upgrade streaming standby + # places user files: 'mirror' (match the + # primary), 'copy', 'clone', + # 'copy_file_range', 'link', 'swap' + # (change requires restart) +#pg_upgrade_standby_old_datadir = '' # retained pre-upgrade data directory to + # link user relations from + # (change requires restart) #hot_standby = on # "off" disallows queries during recovery # (change requires restart) #max_standby_archive_delay = 30s # max delay before canceling queries diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 9d95c5301d..3d8c087b81 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -229,6 +229,12 @@ static const char *const backend_options = "--single -F -O -j -c search_path=pg_ /* Additional switches to pass to backend (either boot or standalone) */ static char *extra_options = ""; +/* + * The cluster subdirectories created here are mirrored by + * SynthesizeUpgradeStreamControlFile() in xlog.c, which builds a bare skeleton + * for a --wal-upgrade streaming standby without running initdb. Keep the two + * lists in sync: a new required subdir added here must be added there too. + */ static const char *const subdirs[] = { "global", "pg_wal/archive_status", diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c index fe5fc5ec13..0484582e75 100644 --- a/src/bin/pg_controldata/pg_controldata.c +++ b/src/bin/pg_controldata/pg_controldata.c @@ -65,6 +65,8 @@ dbState(DBState state) return _("in archive recovery"); case DB_IN_PRODUCTION: return _("in production"); + case DB_IN_UPGRADE: + return _("in pg_upgrade"); } return _("unrecognized status code"); } @@ -349,6 +351,8 @@ main(int argc, char *argv[]) ControlFile->data_checksum_version); printf(_("Default char data signedness: %s\n"), (ControlFile->default_char_signedness ? _("signed") : _("unsigned"))); + printf(_("wal-upgrade window finalized: %s\n"), + (ControlFile->upgrade_finalized ? _("yes") : _("no"))); printf(_("Mock authentication nonce: %s\n"), mock_auth_nonce_str); return 0; diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c index 7dc6e1ad8f..1f3daf14f0 100644 --- a/src/bin/pg_ctl/pg_ctl.c +++ b/src/bin/pg_ctl/pg_ctl.c @@ -269,6 +269,23 @@ get_pgpid(bool is_status_request) if (stat(version_file, &statbuf) != 0 && errno == ENOENT) { + char sigpath[MAXPGPATH]; + struct stat sigbuf; + + /* + * A --wal-upgrade streaming standby may be staged without initdb -- a + * bare directory carrying only config plus the pg_upgrade.signal + * sentinel. PG_VERSION does not exist yet; the postmaster + * synthesizes it (and pg_control) on start. So if the sentinel is + * present, do NOT reject the directory here -- there is simply no + * server running yet, so report "no pid" (0) exactly as for a normal + * not-yet-started cluster and let do_start() launch the postmaster, + * which does the synthesis. + */ + snprintf(sigpath, sizeof(sigpath), "%s/pg_upgrade.signal", pg_data); + if (stat(sigpath, &sigbuf) == 0) + return 0; + write_stderr(_("%s: directory \"%s\" is not a database cluster directory\n"), progname, pg_data); exit(is_status_request ? 4 : 1); diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index f8d25afed9..5d6e41662e 100644 --- a/src/bin/pg_resetwal/pg_resetwal.c +++ b/src/bin/pg_resetwal/pg_resetwal.c @@ -97,6 +97,15 @@ static int wal_segsize_val; static bool char_signedness_given = false; static bool char_signedness_val; +/* + * --wal-upgrade-exact: make the -l/--next-wal-file target authoritative so the + * new WAL start position can be forced DOWN (not just floored upward over the + * existing pg_wal/ segments and the current checkpoint). Intended for internal + * use by "pg_upgrade --wal-upgrade", which must land the end-of-upgrade + * checkpoint (CN) at a byte-deterministic segment boundary. + */ +static bool wal_upgrade_exact = false; + static TimeLineID minXlogTli = 0; static XLogSegNo minXlogSegNo = 0; @@ -135,6 +144,7 @@ main(int argc, char *argv[]) {"next-transaction-id", required_argument, NULL, 'x'}, {"wal-segsize", required_argument, NULL, 1}, {"char-signedness", required_argument, NULL, 2}, + {"wal-upgrade-exact", no_argument, NULL, 3}, {NULL, 0, NULL, 0} }; @@ -352,6 +362,10 @@ main(int argc, char *argv[]) break; } + case 3: + wal_upgrade_exact = true; + break; + default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -378,6 +392,13 @@ main(int argc, char *argv[]) exit(1); } + /* + * --wal-upgrade-exact makes the -l target authoritative, so it is + * meaningless (and almost certainly a mistake) without -l. + */ + if (wal_upgrade_exact && log_fname == NULL) + pg_fatal("option --wal-upgrade-exact requires -l/--next-wal-file"); + /* * Don't allow pg_resetwal to be run as root, to avoid overwriting the * ownership of files in the data directory. We need only check for root @@ -510,7 +531,18 @@ main(int argc, char *argv[]) if (char_signedness_given) ControlFile.default_char_signedness = char_signedness_val; - if (minXlogSegNo > newXlogSegNo) + if (wal_upgrade_exact) + { + /* + * The -l target is authoritative: force the new WAL start to exactly + * that segment, overriding the FindEndOfXLOG floor. This is the only + * way to move the position DOWN (below the current checkpoint / the + * highest existing pg_wal/ segment), which pg_upgrade --wal-upgrade + * needs to land CN at a byte-deterministic boundary. + */ + newXlogSegNo = minXlogSegNo; + } + else if (minXlogSegNo > newXlogSegNo) newXlogSegNo = minXlogSegNo; if (noupdate) @@ -1239,6 +1271,14 @@ usage(void) printf(_(" --char-signedness=OPTION set char signedness to \"signed\" or \"unsigned\"\n")); printf(_(" --wal-segsize=SIZE size of WAL segments, in megabytes\n")); + /* + * --wal-upgrade-exact is intentionally NOT listed here: it is an internal + * implementation detail of pg_upgrade --wal-upgrade (it forces the -l + * target downward so the end-of-upgrade checkpoint lands at a + * byte-deterministic boundary), not a user-facing option. Kept + * undocumented like other binary-upgrade-only machinery. + */ + printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); } diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile index 771addb675..e37097bcc3 100644 --- a/src/bin/pg_upgrade/Makefile +++ b/src/bin/pg_upgrade/Makefile @@ -24,6 +24,7 @@ OBJS = \ parallel.o \ pg_upgrade.o \ relfilenumber.o \ + revertable.o \ server.o \ slru_io.o \ tablespace.o \ diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 2155b01b11..5f08315e90 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -560,6 +560,14 @@ check_and_dump_old_cluster(void) */ get_db_rel_and_slot_infos(&old_cluster); + /* + * On the --wal-upgrade path, also gather the old cluster's physical + * replication slots so they can be recreated on the new cluster (see + * get_old_cluster_physical_slot_infos). Must run while the old server is + * up. + */ + get_old_cluster_physical_slot_infos(); + init_tablespaces(); get_loadable_libraries(); @@ -760,6 +768,12 @@ output_completion_banner(char *deletion_script_file_name) appendPQExpBufferChar(&user_specification, ' '); } + /* report WAL generated during pg_restore (only for --wal-upgrade) */ + if (user_opts.wal_upgrade) + pg_log(PG_REPORT, + "WAL generated during schema restore: " UINT64_FORMAT " bytes", + log_opts.pg_upgrade_wal_bytes); + pg_log(PG_REPORT, "Some statistics are not transferred by pg_upgrade.\n" "Once you start the new server, consider running these two commands:\n" @@ -2061,6 +2075,39 @@ check_new_cluster_replication_slots(void) int i_nslots_on_new; int i_rdt_slot_on_new; + /* + * --wal-upgrade migrates the old cluster's physical replication slots and + * recreates them on the new cluster (see + * get_old_cluster_physical_slot_infos / pg_upgrade.c). Each recreation + * needs a free slot, so the new cluster's max_replication_slots must + * accommodate them. This is independent of the logical-slot / + * retain_dead_tuples machinery below (which early-returns when there are + * no logical slots and is gated on PG17+), so check it up front and for + * any old major. Recreation is otherwise best-effort (warn-not-fail), so + * without this a too-small max_replication_slots would silently migrate + * fewer physical slots than expected and quietly break those standbys' + * streaming. + */ + if (user_opts.wal_upgrade && old_cluster.phys_slot_arr.nslots > 0) + { + PGconn *pconn = connectToServer(&new_cluster, "template1"); + PGresult *pres = executeQueryOrDie(pconn, + "SELECT setting FROM pg_settings " + "WHERE name = 'max_replication_slots'"); + int max_slots; + + if (PQntuples(pres) != 1) + pg_fatal("could not determine \"max_replication_slots\" on the new cluster"); + max_slots = atoi(PQgetvalue(pres, 0, 0)); + PQclear(pres); + PQfinish(pconn); + + if (old_cluster.phys_slot_arr.nslots > max_slots) + pg_fatal("\"max_replication_slots\" (%d) must be greater than or equal to the number of " + "physical replication slots (%d) in the old cluster migrated by --wal-upgrade", + max_slots, old_cluster.phys_slot_arr.nslots); + } + /* * Logical slots can be migrated since PG17 and a physical slot * CONFLICT_DETECTION_SLOT can be migrated since PG19. diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c index 02ea02df60..4b111d7ee6 100644 --- a/src/bin/pg_upgrade/controldata.c +++ b/src/bin/pg_upgrade/controldata.c @@ -75,6 +75,15 @@ get_control_data(ClusterInfo *cluster) int rc; bool live_check = (cluster == &old_cluster && user_opts.live_check); + /* + * With --initdb the old cluster's control data is read early, to derive + * initdb options, before the normal call in + * check_cluster_compatibility(). A non-zero ctrl_ver means it has already + * been read; don't repeat it. + */ + if (cluster->controldata.ctrl_ver != 0) + return; + /* * Because we test the pg_resetwal output as strings, it has to be in * English. Copied from pg_regress.c. diff --git a/src/bin/pg_upgrade/file.c b/src/bin/pg_upgrade/file.c index af82c0de49..9c93b51e3f 100644 --- a/src/bin/pg_upgrade/file.c +++ b/src/bin/pg_upgrade/file.c @@ -21,6 +21,7 @@ #endif #include "common/file_perm.h" +#include "common/file_utils.h" #include "pg_upgrade.h" @@ -35,36 +36,17 @@ void cloneFile(const char *src, const char *dst, const char *schemaName, const char *relName) { -#if defined(HAVE_COPYFILE) && defined(COPYFILE_CLONE_FORCE) - if (copyfile(src, dst, NULL, COPYFILE_CLONE_FORCE) < 0) - pg_fatal("error while cloning relation \"%s.%s\" (\"%s\" to \"%s\"): %m", - schemaName, relName, src, dst); -#elif defined(__linux__) && defined(FICLONE) - int src_fd; - int dest_fd; - - if ((src_fd = open(src, O_RDONLY | PG_BINARY, 0)) < 0) - pg_fatal("error while cloning relation \"%s.%s\": could not open file \"%s\": %m", - schemaName, relName, src); - - if ((dest_fd = open(dst, O_RDWR | O_CREAT | O_EXCL | PG_BINARY, - pg_file_create_mode)) < 0) - pg_fatal("error while cloning relation \"%s.%s\": could not create file \"%s\": %m", - schemaName, relName, dst); + int save_errno; - if (ioctl(dest_fd, FICLONE, src_fd) < 0) + switch (pg_clone_file(src, dst, &save_errno)) { - int save_errno = errno; - - unlink(dst); - - pg_fatal("error while cloning relation \"%s.%s\" (\"%s\" to \"%s\"): %s", - schemaName, relName, src, dst, strerror(save_errno)); + case PG_REFLINK_OK: + case PG_REFLINK_UNSUPPORTED: + break; + case PG_REFLINK_ERROR: + pg_fatal("error while cloning relation \"%s.%s\" (\"%s\" to \"%s\"): %s", + schemaName, relName, src, dst, strerror(save_errno)); } - - close(src_fd); - close(dest_fd); -#endif } @@ -147,32 +129,17 @@ void copyFileByRange(const char *src, const char *dst, const char *schemaName, const char *relName) { -#ifdef HAVE_COPY_FILE_RANGE - int src_fd; - int dest_fd; - ssize_t nbytes; - - if ((src_fd = open(src, O_RDONLY | PG_BINARY, 0)) < 0) - pg_fatal("error while copying relation \"%s.%s\": could not open file \"%s\": %m", - schemaName, relName, src); - - if ((dest_fd = open(dst, O_RDWR | O_CREAT | O_EXCL | PG_BINARY, - pg_file_create_mode)) < 0) - pg_fatal("error while copying relation \"%s.%s\": could not create file \"%s\": %m", - schemaName, relName, dst); + int save_errno; - do + switch (pg_copy_file_range_all(src, dst, &save_errno)) { - nbytes = copy_file_range(src_fd, NULL, dest_fd, NULL, SSIZE_MAX, 0); - if (nbytes < 0) - pg_fatal("error while copying relation \"%s.%s\": could not copy file range from \"%s\" to \"%s\": %m", - schemaName, relName, src, dst); + case PG_REFLINK_OK: + case PG_REFLINK_UNSUPPORTED: + break; + case PG_REFLINK_ERROR: + pg_fatal("error while copying relation \"%s.%s\": could not copy file range from \"%s\" to \"%s\": %s", + schemaName, relName, src, dst, strerror(save_errno)); } - while (nbytes > 0); - - close(src_fd); - close(dest_fd); -#endif } diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c index 37fff93892..8a69148dd5 100644 --- a/src/bin/pg_upgrade/info.c +++ b/src/bin/pg_upgrade/info.c @@ -21,7 +21,6 @@ static void create_rel_filename_map(const char *old_data, const char *new_data, static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db); static void free_db_and_rel_infos(DbInfoArr *db_arr); -static void get_template0_info(ClusterInfo *cluster); static void get_db_infos(ClusterInfo *cluster); static char *get_rel_infos_query(void); static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg); @@ -328,7 +327,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster) * Get information about template0, which will be copied from the old cluster * to the new cluster. */ -static void +void get_template0_info(ClusterInfo *cluster) { PGconn *conn = connectToServer(cluster, "template1"); @@ -832,6 +831,90 @@ count_old_cluster_logical_slots(void) return slot_count; } +/* + * get_old_cluster_physical_slot_infos() + * + * Gather the old cluster's physical replication slots so --wal-upgrade can + * recreate them on the new cluster. Stock pg_upgrade does NOT migrate physical + * slots: before --wal-upgrade a standby could not follow the upgrade at all + * (it was rebuilt from scratch), so its slot was pointless to carry. With + * --wal-upgrade the standby follows by streaming the upgrade window, so its + * slot becomes meaningful across the boundary -- migrating it preserves both + * the standby's slot identity (HA tooling / primary_slot_name keep working) + * and, because the recreated slot is reserved before CN, the window retention + * itself. It is the only slot that pins the window; with no physical slot, + * --wal-upgrade has no streaming consumer and the window is not pinned. + * + * Physical slots are cluster-wide (not database-scoped) and carry no decoding + * state, so a single query over pg_replication_slots suffices. Temporary and + * invalidated ("lost") slots are skipped: temporary slots cannot survive the + * start/stop cycles of the upgrade, and an invalidated slot has no usable + * restart_lsn to recreate. Assumes the old server is running. + */ +void +get_old_cluster_physical_slot_infos(void) +{ + PGconn *conn; + PGresult *res; + PhysicalSlotInfo *slotinfos = NULL; + int num_slots; + + old_cluster.phys_slot_arr.slots = NULL; + old_cluster.phys_slot_arr.nslots = 0; + + /* Only relevant to --wal-upgrade. */ + if (!user_opts.wal_upgrade) + return; + + conn = connectToServer(&old_cluster, "template1"); + + /* + * Skip invalidated slots so we do not try to recreate a dead one. The + * pg_replication_slots.invalidation_reason column only exists in PG17+, + * so gate the predicate on the old cluster's version -- otherwise the + * query would error on an older major and abort the whole upgrade. On + * older majors we simply enumerate all physical slots; recreation below + * is best-effort and warns rather than fails, so a slot that turns out to + * be unusable does not stop the upgrade. + * + * Exclude the internal "pg_conflict_detection" slot: since PG19 the + * conflict-detection slot for retain_dead_tuples subscriptions is a + * cluster-wide (physical) slot, so it shows up here, but its name is + * reserved -- pg_create_physical_replication_slot() rejects it -- and + * pg_upgrade recreates it separately via its own path. Migrating it here + * would only emit a spurious warning and, if it were the sole physical + * slot, make us believe a standby consumer exists (and try to pin the + * window) when none does. + */ + res = executeQueryOrDie(conn, + "SELECT slot_name " + "FROM pg_catalog.pg_replication_slots " + "WHERE slot_type = 'physical' AND " + "temporary IS FALSE AND " + "slot_name <> 'pg_conflict_detection'%s", + GET_MAJOR_VERSION(old_cluster.major_version) >= 1700 ? + " AND invalidation_reason IS NULL" : ""); + + num_slots = PQntuples(res); + + if (num_slots) + { + int i_slotname = PQfnumber(res, "slot_name"); + + slotinfos = pg_malloc_array(PhysicalSlotInfo, num_slots); + + for (int slotnum = 0; slotnum < num_slots; slotnum++) + slotinfos[slotnum].slotname = + pg_strdup(PQgetvalue(res, slotnum, i_slotname)); + } + + PQclear(res); + PQfinish(conn); + + old_cluster.phys_slot_arr.slots = slotinfos; + old_cluster.phys_slot_arr.nslots = num_slots; +} + /* * get_subscription_info() * diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build index ffbf6ae8d7..774bf46c78 100644 --- a/src/bin/pg_upgrade/meson.build +++ b/src/bin/pg_upgrade/meson.build @@ -14,6 +14,7 @@ pg_upgrade_sources = files( 'parallel.c', 'pg_upgrade.c', 'relfilenumber.c', + 'revertable.c', 'server.c', 'slru_io.c', 'tablespace.c', @@ -69,6 +70,9 @@ tests += { 't/006_transfer_modes.pl', 't/007_multixact_conversion.pl', 't/008_extension_control_path.pl', + 't/009_initdb_option.pl', + 't/010_wal_upgrade.pl', + 't/011_wal_upgrade_standby.pl', ], 'deps': [test_ext], 'test_kwargs': {'priority': 40}, # pg_upgrade tests are slow diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c index f01d2f92d9..dd097f2be0 100644 --- a/src/bin/pg_upgrade/option.c +++ b/src/bin/pg_upgrade/option.c @@ -63,6 +63,12 @@ parseCommandLine(int argc, char *argv[]) {"no-statistics", no_argument, NULL, 5}, {"set-char-signedness", required_argument, NULL, 6}, {"swap", no_argument, NULL, 7}, + {"initdb", no_argument, NULL, 8}, + /* capture the upgrade as WAL and reconstruct it on first startup */ + {"wal-upgrade", no_argument, NULL, 9}, + + /* --wal-upgrade lifecycle subcommand (acts on -d old) */ + {"wal-upgrade-signal-handoff", no_argument, NULL, 14}, {NULL, 0, NULL, 0} }; @@ -234,6 +240,20 @@ parseCommandLine(int argc, char *argv[]) user_opts.transfer_mode = TRANSFER_MODE_SWAP; break; + case 8: + user_opts.initdb_new_cluster = true; + break; + + /* --wal-upgrade */ + case 9: + user_opts.wal_upgrade = true; + break; + + /* --wal-upgrade lifecycle subcommand */ + case 14: + user_opts.revertable_op = REVERTABLE_OP_SIGNAL_HANDOFF; + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), os_info.progname); @@ -244,6 +264,31 @@ parseCommandLine(int argc, char *argv[]) if (optind < argc) pg_fatal("too many command-line arguments (first is \"%s\")", argv[optind]); + /* + * --check is read-only and may run against a live old cluster, while + * --initdb creates the new cluster on disk. Reject the combination. + */ + if (user_opts.check && user_opts.initdb_new_cluster) + pg_fatal("options %s and %s cannot be used together", + "-c/--check", "--initdb"); + + /* + * -O is accepted together with --initdb. create_new_cluster_via_initdb() + * derives initdb's core options from the old cluster's control data, then + * appends -O (new_cluster.pgopts) to the initdb command line; the same -O + * options are also passed to the new cluster's postmaster during the + * server phases. This is needed for --wal-upgrade -- e.g. "-c + * allow_in_place_tablespaces=on" -- so it is not rejected. + */ + + /* + * --swap is compatible with --wal-upgrade. The WAL window is generated + * regardless of transfer mode, so standbys can still be reconstructed + * from it. --swap moves the old cluster's data directories into the new + * cluster (do_swap); this is acceptable because --wal-upgrade offers no + * revert-to-old interface that would depend on the old cluster surviving. + */ + if (!user_opts.sync_method) user_opts.sync_method = pg_strdup("fsync"); @@ -264,6 +309,24 @@ parseCommandLine(int argc, char *argv[]) else setenv("PGOPTIONS", FIX_DEFAULT_READ_ONLY, 1); + /* + * --wal-upgrade-signal-handoff acts on a single existing (running) old + * cluster and does not run an upgrade, so it needs only the old cluster's + * data + bin directories, not the full old/new set the normal flow + * requires. It connects to the LIVE old primary, writes the handoff + * trigger into its WAL, and shuts the primary down at that point so no + * transaction can append WAL after the handoff. The old bin dir is + * needed to run that cluster's pg_ctl for the shutdown. + */ + if (user_opts.revertable_op == REVERTABLE_OP_SIGNAL_HANDOFF) + { + if (old_cluster.pgdata == NULL) + pg_fatal("--wal-upgrade-signal-handoff requires the old cluster data directory (-d/--old-datadir)"); + if (old_cluster.bindir == NULL) + pg_fatal("--wal-upgrade-signal-handoff requires the old cluster bin directory (-b/--old-bindir)"); + return; + } + /* Get values from env if not already set */ check_required_directory(&old_cluster.bindir, "PGBINOLD", false, "-b", _("old cluster binaries reside"), false); @@ -328,6 +391,14 @@ usage(void) printf(_(" --clone clone instead of copying files to new cluster\n")); printf(_(" --copy copy files to new cluster (default)\n")); printf(_(" --copy-file-range copy files to new cluster with copy_file_range\n")); + /* --wal-upgrade usage */ + printf(_(" --wal-upgrade capture the whole upgrade as WAL and reconstruct\n" + " the cluster from it on first startup (atomic,\n" + " crash-safe, WAL-replayable)\n")); + printf(_(" --wal-upgrade-signal-handoff signal streaming standbys to stand down for a\n" + " --wal-upgrade, then shut the old primary down\n")); + printf(_(" --initdb create the new cluster with initdb before\n" + " upgrading (settings derived from old cluster)\n")); printf(_(" --no-statistics do not import statistics from old cluster\n")); printf(_(" --set-char-signedness=OPTION set new cluster char signedness to \"signed\" or\n" " \"unsigned\"\n")); @@ -336,7 +407,9 @@ usage(void) printf(_(" -?, --help show this help, then exit\n")); printf(_("\n" "Before running pg_upgrade you must:\n" - " create a new database cluster (using the new version of initdb)\n" + " create a new database cluster (using the new version of initdb),\n" + " unless the --initdb option is given, in which case pg_upgrade\n" + " creates the new cluster for you\n" " shutdown the postmaster servicing the old cluster\n" " shutdown the postmaster servicing the new cluster\n")); printf(_("\n" diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index 7366fd4627..98611335a3 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -41,14 +41,18 @@ #include "postgres_fe.h" +#include #include #include "access/multixact.h" #include "catalog/pg_class_d.h" +#include "catalog/pg_collation_d.h" #include "common/file_perm.h" #include "common/logging.h" #include "common/restricted_token.h" #include "fe_utils/string_utils.h" +#include "fe_utils/version.h" +#include "mb/pg_wchar.h" #include "pg_upgrade.h" /* @@ -67,6 +71,10 @@ static void copy_xact_xlog_xid(void); static void set_frozenxids(void); static void make_outputdirs(char *pgdata); static void setup(char *argv0); +static void resolve_new_bindir(const char *argv0); +static void create_new_cluster_via_initdb(const char *argv0); +static char *detect_old_cluster_archive_command(void); +static void write_wal_upgrade_archive_conf(const char *archive_command); static void create_logical_replication_slots(void); static void create_conflict_detection_slot(void); @@ -74,6 +82,30 @@ ClusterInfo old_cluster, new_cluster; OSInfo os_info; +/* + * For --wal-upgrade, the old cluster's archive_command carried forward to the + * new cluster (NULL if it was not archiving). Detected while the old server is + * up; used to archive the upgrade window so PITR spans the upgrade. + */ +static char *old_cluster_archive_command = NULL; + +/* + * Fault-injection hook: when set, suppress the XLOG_UPGRADE_COMPLETE record + * (and its durable marker), leaving an incomplete window as a crash mid-window + * would. Gated on USE_ASSERT_CHECKING, so a production (non-assert) build + * ignores the environment variable entirely and can never emit an incomplete + * window. + */ +static inline bool +wal_upgrade_skip_complete(void) +{ +#ifdef USE_ASSERT_CHECKING + return getenv("PG_UPGRADE_TEST_SKIP_COMPLETE") != NULL; +#else + return false; +#endif +} + char *output_files[] = { SERVER_LOG_FILE, #ifdef WIN32 @@ -102,11 +134,29 @@ main(int argc, char **argv) /* Set default restrictive mask until new cluster permissions are read */ umask(PG_MODE_MASK_OWNER); + parseCommandLine(argc, argv); + /* + * A --wal-upgrade lifecycle subcommand acts on the running old cluster + * and exits without running an upgrade. new_cluster.bindir locates + * pg_ctl. + */ + if (user_opts.revertable_op != REVERTABLE_OP_NONE) + { + resolve_new_bindir(argv[0]); + perform_revertable_op(); + return 0; + } + + get_restricted_token(); adjust_data_dir(&old_cluster); + + if (user_opts.initdb_new_cluster) + create_new_cluster_via_initdb(argv[0]); + adjust_data_dir(&new_cluster); /* @@ -136,6 +186,12 @@ main(int argc, char **argv) check_cluster_compatibility(); + /* + * The --wal-upgrade recovery anchor (CN) is the checkpoint taken just + * before the end-of-upgrade full-page image burst, not the initdb + * checkpoint. See the end-of-upgrade block below. + */ + check_and_dump_old_cluster(); @@ -165,21 +221,53 @@ main(int argc, char **argv) /* New now using xids of the old system */ - /* -- NEW -- */ + /* + * For --wal-upgrade the new cluster runs at wal_level=replica (initdb's + * default), so the end-of-upgrade full-page images -- and the persisted + * pg_control -- are at a level a standby can recover from. Recovery + * anchors at CN and never replays pg_restore's WAL, so restore-phase WAL + * is throwaway. + */ start_postmaster(&new_cluster, true); prepare_new_globals(); + /* + * For --wal-upgrade no WAL markers are emitted here; the entire upgrade + * image is captured as full-page images at the very end, once everything + * is on disk and a CHECKPOINT has flushed all buffers. See below. + */ + create_new_objects(); + /* + * Stop the server before transferring relation files, as stock pg_upgrade + * does. The transfer overwrites the pg_restore-built files by a raw + * copy/clone/link that bypasses the buffer manager; if the server were + * up, the capture-time CHECKPOINT could flush stale dirty pages back over + * the transferred files. For --wal-upgrade the server restarts with a + * clean buffer pool, so the capture reads exactly the transferred files. + */ stop_postmaster(false); + /* + * The new cluster keeps a fresh system identifier, as stock pg_upgrade + * does. Recovery of the upgrade window only requires pg_control and the + * window's WAL to agree on the sysid, not that it match the old cluster + * -- see the "Resetting WAL archives" step in copy_xact_xlog_xid(). + */ + /* * Most failures happen in create_new_objects(), which has completed at * this point. We do this here because it is just before file transfer, * which for --link will make it unsafe to start the old cluster once the * new cluster is started, and for --swap will make it unsafe to start the * old cluster at all. + * + * As in upstream, --link and --swap disable the old cluster while --copy + * and --clone leave it intact. --wal-upgrade does not change this: the + * upgrade still generates the WAL window regardless, so standbys are + * re-provisioned by streaming it from the upgraded primary. */ if (user_opts.transfer_mode == TRANSFER_MODE_LINK || user_opts.transfer_mode == TRANSFER_MODE_SWAP) @@ -189,20 +277,354 @@ main(int argc, char **argv) old_cluster.pgdata, new_cluster.pgdata); /* - * Assuming OIDs are only used in system tables, there is no need to - * restore the OID counter because we have not transferred any OIDs from - * the old system, but we do it anyway just in case. We do it late here - * because there is no need to have the schema load use new oids. + * Set the new cluster's next OID (stock upstream step, run with the + * server down and after the restore, which consumes OIDs). For + * --wal-upgrade it must happen before the end-of-upgrade checkpoint below + * so that checkpoint (CN) records the transplanted OID counter; recovery + * from CN then reproduces the counters. + * + * On the --wal-upgrade STREAMING path (no archive_command carried + * forward) this reset does double duty: it also positions the new + * cluster's WAL at the old cluster's next segment (nextxlogfile, timeline + * 1) so the reset-written DB_SHUTDOWNED checkpoint -- redo == checkPoint + * == the segment boundary -- becomes a byte-deterministic CN. A fresh + * standby skeleton can then derive CN LOCALLY from its retained old + * datadir (nextxlogfile) with no anchor round-trip. A plain pg_resetwal + * -l only floors the start upward (it maxes -l over the current + * checkpoint redo and existing pg_wal/ segments), and after the restore + * the checkpoint is at a high segment, so -l alone would be ignored; + * --wal-upgrade-exact makes the -l target authoritative and forces the + * position DOWN to nextxlogfile (pg_resetwal's own KillExistingXLOG then + * deletes the restore's leftover segments, so no separate pg_wal/ + * emptying is needed). On the archive path CN need not be + * byte-deterministic (PITR locates it by scanning the WAL), so the plain + * reset is used. */ - prep_status("Setting next OID for new cluster"); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, - "\"%s/pg_resetwal\" -o %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_nxtoid, - new_cluster.pgdata); - check_ok(); + if (user_opts.wal_upgrade && old_cluster_archive_command == NULL) + { + prep_status("Setting next OID and CN log position for new cluster"); + exec_prog(UTILITY_LOG_FILE, NULL, true, true, + /* timeline 1 to match controldata and emit no WAL history file */ + "\"%s/pg_resetwal\" --wal-upgrade-exact -o %u -l 00000001%s \"%s\"", + new_cluster.bindir, old_cluster.controldata.chkpnt_nxtoid, + old_cluster.controldata.nextxlogfile + 8, + new_cluster.pgdata); + check_ok(); + } + else + { + prep_status("Setting next OID for new cluster"); + exec_prog(UTILITY_LOG_FILE, NULL, true, true, + "\"%s/pg_resetwal\" -o %u \"%s\"", + new_cluster.bindir, old_cluster.controldata.chkpnt_nxtoid, + new_cluster.pgdata); + check_ok(); + } migrate_logical_slots = count_old_cluster_logical_slots(); + if (user_opts.wal_upgrade) + { + PGconn *conn; + + /* Window's last segment; the archive barrier waits for it to drain. */ + char upgrade_window_last_seg[MAXPGPATH] = {0}; + + /* + * The old cluster's archive_command is detected and carried forward + * only on the --initdb path (create_new_cluster_via_initdb). Without + * it, the upgrade window is generated but never archived, so a PITR + * base backup could not roll across the upgrade boundary, which is + * the purpose of --wal-upgrade, while the run still reported success. + * Emit a warning rather than produce an upgrade that cannot be + * recovered by PITR. + */ + if (old_cluster_archive_command == NULL) + pg_log(PG_WARNING, + "--wal-upgrade did not configure WAL archiving for the new cluster; " + "the upgrade window will not be archived and cannot be recovered by PITR. " + "Use --initdb so the old cluster's archive_command is carried forward, " + "or configure archiving on the new cluster before it is needed."); + + /* + * Restart with a fresh buffer pool for the WAL capture phase. The + * server now reads the just-transplanted counters from pg_control, so + * the checkpoint we take below captures them. + */ + start_postmaster(&new_cluster, true); + conn = connectToServer(&new_cluster, "template1"); + + /* + * Everything below is emitted after all pg_upgrade work is complete, + * so the upgrade's own WAL is generated as one atomic burst at the + * end rather than interleaved with the restore. + * + * When the window has a consumer, it must be pinned in pg_wal/ BEFORE + * the CN checkpoint by a slot whose restart_lsn sits at or before CN: + * a streaming standby anchors recovery at CN, so CN and the whole + * window must survive; a restart_lsn past CN could let CN be + * recycled. The pin is size-independent (see server.c's + * max_slot_wal_keep_size=-1). + * + * The consumer, if any, determines whether a slot is needed: + * + * - The old cluster HAD physical slots (a standby was connected). We + * migrate them here with immediately_reserve = true, which reserves + * each slot's restart_lsn at the burst server's CURRENT insert + * position. Correctness of the pin (restart_lsn <= CN) rests + * entirely on this loop running BEFORE the CN checkpoint is emitted + * just below (binary_upgrade_emit_wal_window); the migrated slots + * carry only the slot NAME across, not the old restart_lsn, so the + * reserve happens here and now. Every migrated slot reserves + * at/before CN, so the window is pinned at the minimum of their + * restart_lsns (all <= CN) -- it does not matter which one; any one + * suffices. Keep the create loop ahead of the emit call: moving it + * after CN would silently break retention. The standby reconnects + * with its own real primary_slot_name, and the standby-provisioning + * lifecycle drops the slot once caught up. + * + * - No physical slot, but archiving is configured (archive-PITR). No + * slot is needed: the window's durable home is the archive, not + * pg_wal/. Generation cannot recycle it (the burst server uses + * max_wal_size=1TB / checkpoint_timeout=1h so no checkpoint fires + * between CN and shutdown), and the wait-for-archive barrier below + * guarantees CN..COMPLETE reaches the archive before shutdown; after + * that pg_wal/ recycling is harmless because PITR restores from the + * archive. + * + * - No physical slot AND no archiving. Then --wal-upgrade has no + * consumer for the window at all: no standby will stream it (a slot + * is the operator's declaration that one is expected) and no PITR + * will read it. The primary keeps its transferred files and + * auto-serves exactly as a plain pg_upgrade would, so the window is + * simply unused; we do NOT pin it, and it is recycled as ordinary + * WAL. A standby wanted later is built conventionally (fresh base + * backup), which does not need the window. + * + * So the dedicated UPGRADE_WINDOW_SLOT is never created; retention + * comes solely from a migrated physical slot, when one exists. + */ + for (int slotnum = 0; slotnum < old_cluster.phys_slot_arr.nslots; slotnum++) + { + PhysicalSlotInfo *slot = &old_cluster.phys_slot_arr.slots[slotnum]; + PGresult *slotres; + + pg_log(PG_VERBOSE, "migrating physical replication slot \"%s\"", + slot->slotname); + + /* + * Best-effort: a slot that cannot be recreated (e.g. a name + * collision, or a slot that was invalid on an old major where we + * could not filter it out) must not abort the whole upgrade, so + * warn and continue rather than using executeQueryOrDie(). + * Losing a migrated slot only means that standby must be + * re-provisioned conventionally; it does not affect the primary's + * upgrade. + */ + slotres = PQexec(conn, + psprintf("SELECT pg_create_physical_replication_slot('%s', true, false)", + slot->slotname)); + if (PQresultStatus(slotres) != PGRES_TUPLES_OK) + { + pg_log(PG_WARNING, + "could not migrate physical replication slot \"%s\": %s", + slot->slotname, PQerrorMessage(conn)); + pg_log(PG_WARNING, + "The standby that used it must be re-provisioned after the upgrade."); + } + PQclear(slotres); + } + + /* + * Emit the entire upgrade window with a single binary-upgrade-gated + * backend call. binary_upgrade_emit_wal_window() does, in C: + * + * 1. flush SLRU + checkpoint -- that checkpoint IS CN, the recovery + * anchor (the last checkpoint preceding XLOG_UPGRADE_START); no + * separate CHECKPOINT is issued. Replay starts at CN, not the initdb + * checkpoint, applying only the end-of-upgrade full-page images that + * follow -- never pg_restore's CREATE DATABASE FILE_COPY records -- + * so the cluster reconstructs from an empty data directory. 2. + * PG_UPGRADE_START (with the PG_VERSION string). 3. the + * directory-tree after-image (before any file image, so replay + * recreates the skeleton first). 4. RELFILE full-page images + the + * RELINK manifest. 5. SLRU images for pg_xact / pg_multixact. 6. + * PG_UPGRADE_COMPLETE + the durable pg_control finalized flag. + * + * The XID/OID/multixact counters are not emitted separately: they + * were transplanted into pg_control before CN, so the CN checkpoint + * record already carries them. CN's LSN is not captured here; first + * startup (PerformWalUpgradeIfNeeded) derives it from the WAL and + * arms pg_control at CN, which is what lets a physical standby + * recover the same anchor from the streamed WAL. + * + * WAL generation happens entirely in C behind the IsBinaryUpgrade + * gate, so the window-emitting steps are not exposed as reusable SQL + * on a normal cluster. wal_upgrade_skip_complete() (assert builds + * only) suppresses the COMPLETE marker to simulate a crash + * mid-upgrade so first startup FATALs and leaves the old cluster + * intact. + */ + PQclear(executeQueryOrDie(conn, + "SELECT binary_upgrade_emit_wal_window(%u, %u, %d, %s)", + old_cluster.major_version, + new_cluster.major_version, + (int) user_opts.transfer_mode, + wal_upgrade_skip_complete() ? "true" : "false")); + + /* + * Capture the segment holding PG_UPGRADE_COMPLETE before the switch, + * so the wait-for-archive barrier below targets the window's last + * segment (CN..COMPLETE) rather than a later one. pg_switch_wal() + * then seals that segment so it is archivable. + */ + if (old_cluster_archive_command != NULL) + { + PGresult *res; + + res = executeQueryOrDie(conn, + "SELECT pg_walfile_name(pg_current_wal_lsn())"); + strlcpy(upgrade_window_last_seg, PQgetvalue(res, 0, 0), + sizeof(upgrade_window_last_seg)); + PQclear(res); + } + + PQclear(executeQueryOrDie(conn, "SELECT pg_switch_wal()")); + + /* + * When the window is being archived, wait until the archiver has + * drained the window's last segment (holding PG_UPGRADE_COMPLETE, + * captured above) before shutting the burst server down. Only + * CN..COMPLETE must reach the archive; pre-CN pg_restore WAL is + * irrelevant and legitimately recycled. The window is usually + * already archived by now (smart shutdown drains the archiver); this + * barrier makes that explicit. Mirrors do_pg_backup_stop()'s + * waitforarchive spin on pg_stat_archiver.last_archived_wal. + */ + if (old_cluster_archive_command != NULL) + { + PGresult *res; + + /* + * Distinguish a persistent archiving failure from a transient one + * the archiver retries and recovers from. pg_stat_archiver's + * last_failed_wal is a sticky high-water mark: it is set on any + * failure and never cleared on a later success, so it cannot tell + * us whether archiving is *currently* failing. Instead give up + * only when at least one failure has been recorded AND + * last_archived_wal makes no forward progress for a bounded + * number of consecutive polls. A transient failure (retried and + * drained) advances last_archived and resets the stall counter. + */ + char prev_archived[MAXPGPATH] = {0}; + int64 prev_failed = -1; + int stalled_polls = 0; + + /* + * ~30s of no progress while failures mount -> give up (100ms + * poll) + */ +#define UPGRADE_ARCHIVE_STALL_LIMIT 300 + + prep_status("Waiting for the upgrade window to be archived"); + for (;;) + { + char last_archived[MAXPGPATH]; + int64 failed_count; + + res = executeQueryOrDie(conn, + "SELECT coalesce(last_archived_wal, ''), " + "failed_count " + "FROM pg_stat_archiver"); + strlcpy(last_archived, PQgetvalue(res, 0, 0), sizeof(last_archived)); + failed_count = strtoi64(PQgetvalue(res, 0, 1), NULL, 10); + PQclear(res); + + /* + * last_archived_wal advances in segment-name order, so once + * it has reached (>=) the window's last segment, CN..COMPLETE + * is archived. + */ + if (last_archived[0] != '\0' && + strcmp(last_archived, upgrade_window_last_seg) >= 0) + break; + + /* + * Persistent-failure detector. The stall counter is driven + * by ABSENCE OF PROGRESS, not by catching the exact poll on + * which the archiver bumps its failure count: the archiver + * fails far less often than the 100ms poll rate, so keying on + * failed_count > prev_failed would reset the counter on + * almost every poll and never trip. Instead: increment on + * every poll where last_archived_wal did not advance AND at + * least one archive failure has been recorded (failed_count > + * 0); reset the moment last_archived_wal advances. A + * transient failure the archiver recovers from advances + * last_archived and clears the counter; a truly stuck + * archive_command makes no progress and trips the fatal after + * UPGRADE_ARCHIVE_STALL_LIMIT polls. This also covers an + * archive that never produces any segment (fails from the + * first): last_archived stays "" across polls, which is "no + * advance", so the counter still climbs. prev_failed >= 0 + * means we have a prior poll to compare against (skip the + * first). + */ + if (failed_count > 0 && + prev_failed >= 0 && + strcmp(last_archived, prev_archived) == 0) + { + if (++stalled_polls >= UPGRADE_ARCHIVE_STALL_LIMIT) + pg_fatal("archive_command is persistently failing while " + "archiving the upgrade window (no progress past " + "WAL file %s); the upgrade cannot be made " + "recoverable by PITR", + last_archived[0] != '\0' ? last_archived : "(none)"); + } + else + stalled_polls = 0; + + strlcpy(prev_archived, last_archived, sizeof(prev_archived)); + prev_failed = failed_count; + + pg_usleep(100000); /* 100ms */ + } + check_ok(); + + /* + * No retention slot to drop: pg_upgrade never creates a dedicated + * one. A migrated physical slot, if any, belongs to the old + * cluster's standby and is preserved so that standby can + * reconnect after the upgrade. The window (now archived) is free + * to be recycled from pg_wal/ by the auto-served cluster's normal + * checkpointing. + */ + } + + PQfinish(conn); + + /* + * Clean shutdown (-m smart) so the shutdown checkpoint lands past + * XLOG_UPGRADE_COMPLETE. The primary is now a normal, fully-upgraded + * cluster keeping its transferred files on disk (not reconstructed + * from WAL): with the control checkpoint past COMPLETE, first + * startup's PerformWalUpgradeIfNeeded() "already applied" guard + * (checkpoint > CN) skips the window replay. + * + * The window (CN..COMPLETE) stays intact in pg_wal/, pinned by the + * retention slot, so a fresh standby skeleton can stream and replay + * it. Revert-and-replay is thus a standby-only mechanism. + */ + stop_postmaster(false); + + /* + * The durable "window reached COMPLETE" flag in pg_control was + * already set inside binary_upgrade_emit_wal_window() above (on the + * running burst server), which is suppressed by the same + * skip_complete argument -- so a crash-mid-window primary is treated + * as a partial upgrade. Nothing to write here. + */ + } + /* * Migrate replication slots to the new cluster. * @@ -252,7 +674,20 @@ main(int argc, char **argv) create_script_for_old_cluster_deletion(&deletion_script_file_name); - issue_warnings_and_set_wal_level(); + /* + * For --wal-upgrade, skip issue_warnings_and_set_wal_level(): it + * unconditionally starts/stops the server, which would checkpoint and + * recycle the upgrade WAL still needed in pg_wal/. + * + * pg_control is not stamped here. First startup runs + * PerformWalUpgradeIfNeeded(), which derives CN from the WAL, arms + * pg_control at CN in-process, and crash-recovers from CN through + * PG_UPGRADE_COMPLETE. Deriving the anchor from the WAL (rather than + * pre-stamping) lets the same WAL stream drive recovery on a physical + * standby. The whole upgrade is applied in one pass on next startup. + */ + if (!user_opts.wal_upgrade) + issue_warnings_and_set_wal_level(); pg_log(PG_REPORT, "\n" @@ -358,6 +793,263 @@ make_outputdirs(char *pgdata) } +/* + * resolve_new_bindir() + * + * Idempotent helper: if new_cluster.bindir has not been set by the user via + * -B, derive it from the path of the currently executing pg_upgrade binary. + * Called early by create_new_cluster_via_initdb() so that the initdb path + * is available before verify_directories() runs. + */ +static void +resolve_new_bindir(const char *argv0) +{ + if (!new_cluster.bindir) + { + char exec_path[MAXPGPATH]; + + if (find_my_exec(argv0, exec_path) < 0) + pg_fatal("%s: could not find own program executable", argv0); + /* Trim off program name and keep just the directory */ + *last_dir_separator(exec_path) = '\0'; + canonicalize_path(exec_path); + new_cluster.bindir = pg_strdup(exec_path); + } +} + + +/* + * create_new_cluster_via_initdb() + * + * Implements --initdb: run initdb to create the new cluster before upgrading, + * deriving WAL segment size, data checksums, encoding, and locale settings + * from the old cluster so that check_control_data() passes. + * + * This runs before the normal verify_directories() / setup() path, so we + * use a temporary log directory under the new bindir for the early server + * start; make_outputdirs() will replace log_opts.logdir later. + */ +static void +create_new_cluster_via_initdb(const char *argv0) +{ + DbLocaleInfo *locale; + PQExpBufferData cmd; + char tmp_logdir[MAXPGPATH]; + char *saved_logdir = log_opts.logdir; + const char *encoding_name; + + /* Pass argv0 (full path) so find_my_exec can locate the binary */ + resolve_new_bindir(argv0); + + /* + * Verify that initdb is present and executable before doing any work. The + * normal path checks this later inside verify_directories(), but we run + * before that, so fail early with a useful message. + */ + { + char initdb_path[MAXPGPATH]; + + snprintf(initdb_path, sizeof(initdb_path), "%s/initdb", + new_cluster.bindir); + if (validate_exec(initdb_path) != 0) + pg_fatal("could not find \"initdb\" in \"%s\": %m\n" + "The --initdb option requires initdb to be present in the new cluster's bin directory.", + new_cluster.bindir); + } + + old_cluster.major_version = get_pg_version(old_cluster.pgdata, + &old_cluster.major_version_str); + + /* + * get_control_data() selects pg_resetwal vs. pg_resetxlog via + * bin_version, which check_bindir() normally fills in later. Seed it now + * so the right binary name is used in this early call. + */ + if (old_cluster.bin_version == 0) + old_cluster.bin_version = old_cluster.major_version; + + /* + * Refuse to clobber an already-populated new data directory. --initdb is + * meant to create the new cluster from scratch, so an existing PG_VERSION + * there means the operator pointed at the wrong directory or a leftover + * from a previous attempt; either way, silently removing a database + * system is too dangerous. Fail with pg_upgrade's own clear message (not + * initdb's "directory not empty") so the operator can remove it + * deliberately. + */ + { + char verfile[MAXPGPATH]; + struct stat st; + + snprintf(verfile, sizeof(verfile), "%s/PG_VERSION", + new_cluster.pgdata); + if (stat(verfile, &st) == 0) + pg_fatal("new cluster data directory \"%s\" already contains a database system; " + "--initdb requires an empty or nonexistent directory", + new_cluster.pgdata); + } + + get_control_data(&old_cluster); + + /* Set up a temporary log directory for the early server start. */ + snprintf(tmp_logdir, sizeof(tmp_logdir), "%s/pg_upgrade_initdb.log.d", + new_cluster.bindir); + if (mkdir(tmp_logdir, pg_dir_create_mode) < 0 && errno != EEXIST) + pg_fatal("could not create temporary log directory \"%s\": %m", + tmp_logdir); + log_opts.logdir = tmp_logdir; + + if (!old_cluster.sockdir) + old_cluster.sockdir = user_opts.socketdir ? user_opts.socketdir : "."; + + prep_status("Inspecting old cluster locale for new cluster creation"); + start_postmaster(&old_cluster, true); + get_template0_info(&old_cluster); + + /* + * While the old server is up, capture its archive_command so it can be + * carried forward to the new cluster: the upgrade window and post-upgrade + * WAL then flow to the same archive, making the upgrade recoverable by + * ordinary archive-based PITR with no extra operator action. + */ + if (user_opts.wal_upgrade) + old_cluster_archive_command = detect_old_cluster_archive_command(); + stop_postmaster(false); + check_ok(); + + locale = old_cluster.template0; + encoding_name = pg_encoding_to_char(locale->db_encoding); + + prep_status("Creating new cluster with initdb"); + + + initPQExpBuffer(&cmd); + appendPQExpBuffer(&cmd, "\"%s/initdb\" -D \"%s\" -N", + new_cluster.bindir, new_cluster.pgdata); + appendPQExpBuffer(&cmd, " -U \"%s\"", os_info.user); + appendPQExpBuffer(&cmd, " --wal-segsize=%u", + old_cluster.controldata.walseg / (1024 * 1024)); + + /* + * Pass --data-checksums or --no-data-checksums explicitly. Starting from + * PG18, initdb enables checksums by default, so we must mirror the old + * cluster's setting to avoid a mismatch that check_control_data() would + * reject. + */ + if (old_cluster.controldata.data_checksum_version != 0) + appendPQExpBufferStr(&cmd, " --data-checksums"); + else + appendPQExpBufferStr(&cmd, " --no-data-checksums"); + + appendPQExpBuffer(&cmd, " --encoding=%s", encoding_name); + appendPQExpBuffer(&cmd, " --locale-provider=%s", + collprovider_name(locale->db_collprovider)); + appendPQExpBuffer(&cmd, " --lc-collate=\"%s\" --lc-ctype=\"%s\"", + locale->db_collate, locale->db_ctype); + + if (locale->db_locale) + { + if (locale->db_collprovider == COLLPROVIDER_ICU) + appendPQExpBuffer(&cmd, " --icu-locale=\"%s\"", + locale->db_locale); + else if (locale->db_collprovider == COLLPROVIDER_BUILTIN) + appendPQExpBuffer(&cmd, " --builtin-locale=\"%s\"", + locale->db_locale); + } + + if (new_cluster.pgopts) + appendPQExpBuffer(&cmd, " %s", new_cluster.pgopts); + + exec_prog(UTILITY_LOG_FILE, NULL, true, true, "%s", cmd.data); + + termPQExpBuffer(&cmd); + log_opts.logdir = saved_logdir; + + check_ok(); + + /* + * If the old cluster was archiving, enable the same archiving in the new + * cluster's postgresql.conf, so both the burst server and the auto-served + * upgraded cluster archive to the same place. postgresql.conf (rather + * than -o flags) lets an archive_command with spaces and shell + * metacharacters be quoted correctly. + */ + if (old_cluster_archive_command != NULL) + write_wal_upgrade_archive_conf(old_cluster_archive_command); +} + +/* + * Read the old cluster's archive_command over a connection to the running + * old server. Returns a pg_strdup'd copy if archiving was configured + * (archive_mode <> off AND archive_command non-empty), else NULL. Used to + * carry the old cluster's archiving forward to the new cluster. + */ +static char * +detect_old_cluster_archive_command(void) +{ + PGconn *conn = connectToServer(&old_cluster, "template1"); + PGresult *res; + char *mode; + char *cmd; + char *result = NULL; + + res = executeQueryOrDie(conn, + "SELECT current_setting('archive_mode'), " + "current_setting('archive_command')"); + mode = PQgetvalue(res, 0, 0); + cmd = PQgetvalue(res, 0, 1); + + /* + * archive_mode is off/on/always; archive_command defaults to '' (or the + * placeholder "(disabled)" on some builds). Only carry a real command. + */ + if (strcmp(mode, "off") != 0 && + cmd[0] != '\0' && + strcmp(cmd, "(disabled)") != 0) + result = pg_strdup(cmd); + + PQclear(res); + PQfinish(conn); + return result; +} + +/* + * Append archive settings to the new cluster's postgresql.conf so the + * upgrade window and the post-upgrade WAL reach the archive. archive_command + * is the old cluster's command, carried forward. Only called when the old + * cluster was archiving. + */ +static void +write_wal_upgrade_archive_conf(const char *archive_command) +{ + char conf_path[MAXPGPATH]; + FILE *fp; + const char *p; + + snprintf(conf_path, sizeof(conf_path), "%s/postgresql.conf", + new_cluster.pgdata); + + fp = fopen(conf_path, "a"); + if (fp == NULL) + pg_fatal("could not open \"%s\" to enable WAL archiving: %m", conf_path); + + /* archive_command is emitted as a single-quoted GUC value; double any '. */ + fputs("\n# added by pg_upgrade --wal-upgrade (carried from the old cluster)\n" + "archive_mode = on\n" + "archive_command = '", fp); + for (p = archive_command; *p; p++) + { + if (*p == '\'') + fputc('\'', fp); + fputc(*p, fp); + } + fputs("'\n", fp); + + if (fclose(fp) != 0) + pg_fatal("could not write \"%s\": %m", conf_path); +} + + static void setup(char *argv0) { @@ -372,17 +1064,7 @@ setup(char *argv0) * with -B, default to using the path of the currently executed pg_upgrade * binary. */ - if (!new_cluster.bindir) - { - char exec_path[MAXPGPATH]; - - if (find_my_exec(argv0, exec_path) < 0) - pg_fatal("%s: could not find own program executable", argv0); - /* Trim off program name and keep just path */ - *last_dir_separator(exec_path) = '\0'; - canonicalize_path(exec_path); - new_cluster.bindir = pg_strdup(exec_path); - } + resolve_new_bindir(argv0); verify_directories(); @@ -598,6 +1280,11 @@ create_new_objects(void) int dbnum; PGconn *conn_new_template1; + /* LSN snapshot before and after pg_restore to measure WAL generated */ + PGresult *lsn_res; + uint64 lsn_before = 0, + lsn_after = 0; + prep_status_progress("Restoring database schemas in the new cluster"); /* @@ -609,6 +1296,20 @@ create_new_objects(void) */ conn_new_template1 = connectToServer(&new_cluster, "template1"); PQclear(executeQueryOrDie(conn_new_template1, "CHECKPOINT")); + + /* + * Snapshot the WAL position before pg_restore to measure the bytes the + * schema restore generates. Only for --wal-upgrade; otherwise skipped so + * the flow matches stock pg_upgrade. + */ + if (user_opts.wal_upgrade) + { + lsn_res = executeQueryOrDie(conn_new_template1, + "SELECT pg_current_wal_lsn() - '0/0'"); + lsn_before = strtoull(PQgetvalue(lsn_res, 0, 0), NULL, 10); + PQclear(lsn_res); + } + PQfinish(conn_new_template1); /* @@ -714,6 +1415,25 @@ create_new_objects(void) end_progress_output(); check_ok(); + /* + * Measure WAL bytes generated by pg_restore: read the current WAL + * position and compute the delta since lsn_before. Only for + * --wal-upgrade; skipped otherwise so the flow matches stock pg_upgrade. + */ + if (user_opts.wal_upgrade) + { + conn_new_template1 = connectToServer(&new_cluster, "template1"); + lsn_res = executeQueryOrDie(conn_new_template1, + "SELECT pg_current_wal_lsn() - '0/0'"); + lsn_after = strtoull(PQgetvalue(lsn_res, 0, 0), NULL, 10); + PQclear(lsn_res); + PQfinish(conn_new_template1); + + log_opts.pg_upgrade_wal_bytes = lsn_after - lsn_before; + pg_log(PG_VERBOSE, "pg_upgrade_wal_bytes: " UINT64_FORMAT, + log_opts.pg_upgrade_wal_bytes); + } + /* update new_cluster info now that we have objects in the databases */ get_db_rel_and_slot_infos(&new_cluster); } @@ -775,7 +1495,8 @@ copy_xact_xlog_xid(void) prep_status("Setting oldest XID for new cluster"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, "\"%s/pg_resetwal\" -f -u %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_oldstxid, + new_cluster.bindir, + old_cluster.controldata.chkpnt_oldstxid, new_cluster.pgdata); check_ok(); @@ -783,11 +1504,13 @@ copy_xact_xlog_xid(void) prep_status("Setting next transaction ID and epoch for new cluster"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, "\"%s/pg_resetwal\" -f -x %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_nxtxid, + new_cluster.bindir, + old_cluster.controldata.chkpnt_nxtxid, new_cluster.pgdata); exec_prog(UTILITY_LOG_FILE, NULL, true, true, "\"%s/pg_resetwal\" -f -e %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_nxtepoch, + new_cluster.bindir, + old_cluster.controldata.chkpnt_nxtepoch, new_cluster.pgdata); /* must reset commit timestamp limits also */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, @@ -863,7 +1586,20 @@ copy_xact_xlog_xid(void) check_ok(); } - /* now reset the wal archives in the new cluster */ + /* + * Now reset the WAL archives in the new cluster. This positions the new + * cluster's WAL at the old cluster's next segment. + * + * For --wal-upgrade this reset also (re)assigns the new cluster's system + * identifier, not forced to the old cluster's value. It rewrites both + * the control file and the fresh WAL segment header from the same + * ControlFile.system_identifier, so pg_control and the burst WAL stay + * consistent -- all that replay requires (recovery validates the WAL's + * xlp_sysid against pg_control, not its numeric value). A standby is + * re-provisioned from a fresh skeleton stamped with this sysid, so it + * need not match the pre-upgrade cluster. Same "new cluster gets a new + * sysid" behavior as stock pg_upgrade. + */ prep_status("Resetting WAL archives"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, /* use timeline 1 to match controldata and no WAL history file */ diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index d6e5bca579..16e66ec39d 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -157,6 +157,23 @@ typedef struct LogicalSlotInfo *slots; /* array of logical slot infos */ } LogicalSlotInfoArr; +/* + * Physical replication slot info. Unlike logical slots these are + * cluster-wide (not database-scoped) and carry no decoding state, so the + * name is all we need to recreate the slot on the new cluster. Only + * migrated on the --wal-upgrade path (see get_old_cluster_physical_slot_infos). + */ +typedef struct +{ + char *slotname; /* slot name */ +} PhysicalSlotInfo; + +typedef struct +{ + int nslots; /* number of physical slot infos */ + PhysicalSlotInfo *slots; /* array of physical slot infos */ +} PhysicalSlotInfoArr; + /* * The following structure represents a relation mapping. */ @@ -248,6 +265,20 @@ typedef enum TRANSFER_MODE_SWAP, } transferMode; +/* + * Revertable-upgrade lifecycle subcommands. When set (not _NONE), + * pg_upgrade does NOT run an upgrade; it acts on an existing cluster and exits. + */ +typedef enum +{ + REVERTABLE_OP_NONE = 0, /* normal pg_upgrade run */ + REVERTABLE_OP_SIGNAL_HANDOFF, /* emit the handoff trigger into the LIVE + * old primary's WAL; it propagates to + * streaming standbys through the normal + * WAL/replication path, which replay it + * and then stand down before the upgrade */ +} RevertableOp; + /* * Enumeration to denote pg_log modes */ @@ -289,6 +320,8 @@ typedef struct int nsubs; /* number of subscriptions */ bool sub_retain_dead_tuples; /* whether a subscription enables * retain_dead_tuples. */ + PhysicalSlotInfoArr phys_slot_arr; /* physical slots to migrate + * (--wal-upgrade only) */ } ClusterInfo; @@ -306,6 +339,8 @@ typedef struct char *dumpdir; /* Dumps */ char *logdir; /* Log files */ bool isatty; /* is stdout a tty */ + /* WAL bytes generated during pg_restore (schema restore phase) */ + uint64 pg_upgrade_wal_bytes; } LogOpts; @@ -325,6 +360,19 @@ typedef struct int char_signedness; /* default char signedness: -1 for initial * value, 1 for "signed" and 0 for * "unsigned" */ + bool initdb_new_cluster; /* run initdb to create the new cluster + * before upgrading, instead of requiring + * the user to have created it manually */ + + /* + * capture the whole upgrade as a WAL-replayable full-page image at the + * end and skip the on-disk data writes, so first startup reconstructs the + * cluster purely from WAL (atomic, crash-safe, recoverable from an empty + * data directory) + */ + bool wal_upgrade; + /* revertable-upgrade lifecycle subcommand, if any (see enum above) */ + RevertableOp revertable_op; } UserOpts; typedef struct @@ -380,6 +428,11 @@ void check_control_data(ControlData *oldctrl, ControlData *newctrl); void disable_old_cluster(transferMode transfer_mode); +/* revertable.c */ + +void perform_revertable_op(void); + + /* dump.c */ void generate_old_dump(void); @@ -423,7 +476,9 @@ FileNameMap *gen_db_file_maps(DbInfo *old_db, DbInfo *new_db, int *nmaps, const char *old_pgdata, const char *new_pgdata); void get_db_rel_and_slot_infos(ClusterInfo *cluster); +void get_template0_info(ClusterInfo *cluster); int count_old_cluster_logical_slots(void); +void get_old_cluster_physical_slot_infos(void); void get_subscription_info(ClusterInfo *cluster); /* option.c */ diff --git a/src/bin/pg_upgrade/relfilenumber.c b/src/bin/pg_upgrade/relfilenumber.c index 6c467bdc8a..382e03ed65 100644 --- a/src/bin/pg_upgrade/relfilenumber.c +++ b/src/bin/pg_upgrade/relfilenumber.c @@ -614,5 +614,6 @@ transfer_relfile(FileNameMap *map, const char *type_suffix) pg_fatal("should never happen"); break; } + } } diff --git a/src/bin/pg_upgrade/revertable.c b/src/bin/pg_upgrade/revertable.c new file mode 100644 index 0000000000..9c6d0b115c --- /dev/null +++ b/src/bin/pg_upgrade/revertable.c @@ -0,0 +1,129 @@ +/* + * revertable.c + * + * --wal-upgrade lifecycle subcommand (signal-handoff). + * + * A --wal-upgrade new cluster comes up read-write on its first start, as an + * ordinary pg_upgrade does. This file implements the signal-handoff subcommand: + * + * --wal-upgrade-signal-handoff -d old trigger streaming standbys to stand down + * + * (A fresh standby needs no prepare step: with primary_conninfo set it + * derives the upgrade window anchor (CN) locally from its retained old datadir + * and streams the window -- see ArmFromLocalDerivationIfConfigured in + * pgupgrade_wal.c.) + * + * Copyright (c) 2010-2026, PostgreSQL Global Development Group + * src/bin/pg_upgrade/revertable.c + */ + +#include "postgres_fe.h" + +#include /* system() */ +#include /* unlink() */ + +#include "pg_upgrade.h" + +/* + * --wal-upgrade-signal-handoff: connect to the live old primary and write the + * streaming-handoff trigger into its (old-format) WAL. It emits a WAL record + * that propagates to streaming standbys through the normal replication path, + * rather than contacting each standby. On replaying it, a standby shuts down + * cleanly, ready for the new-version binary swap / re-provision. Run this + * before stopping the old + * primary and running pg_upgrade. + * + * Unlike --wal-upgrade itself (which acts on stopped clusters), this one + * requires the old primary to be RUNNING. The target major version passed to + * the trigger is this pg_upgrade binary's own major (the new version the + * standby will converge to). + */ +static void +do_signal_handoff(void) +{ + char sigpath[MAXPGPATH]; + FILE *f; + char cmd[MAXPGPATH * 2]; + int rc; + + if (old_cluster.pgdata == NULL || old_cluster.pgdata[0] == '\0') + pg_fatal("--wal-upgrade-signal-handoff requires the old cluster data directory (-d)"); + if (old_cluster.bindir == NULL || old_cluster.bindir[0] == '\0') + pg_fatal("--wal-upgrade-signal-handoff requires the old cluster bin directory (-b)\n" + "(needed to shut the old primary down at the handoff point)"); + + /* + * Emit the handoff during the primary's own shutdown, not from a client + * session. A client-issued handoff cannot be the guaranteed end of the + * old WAL stream: a backend past its commit point, or a fresh connection + * racing the terminate/shutdown, can flush a commit record after the + * marker, so a standby that stopped at the handoff would be missing an + * acknowledged commit. + * + * Instead drop a sentinel and let the server emit the record itself. On + * a fast shutdown the postmaster drains every backend (flushing their + * commit WAL) before the checkpointer runs ShutdownXLOG(); ShutdownXLOG() + * then emits XLOG_UPGRADE_HANDOFF just before the shutdown checkpoint, + * while WAL senders are still streaming. By construction all user WAL + * precedes the handoff, which precedes the shutdown checkpoint -- so the + * handoff is the exact, race-free end of the old stream that streaming + * standbys stop at. The sentinel records the target major version (this + * pg_upgrade binary's own major, the version the standby converges to). + */ + snprintf(sigpath, sizeof(sigpath), "%s/pg_upgrade_handoff.pending", + old_cluster.pgdata); + + prep_status("Arming pg_upgrade handoff on the old primary"); + f = fopen(sigpath, "w"); + if (f == NULL) + pg_fatal("could not create handoff signal file \"%s\": %m", sigpath); + fprintf(f, "%d\n", PG_MAJORVERSION_NUM); + if (fclose(f) != 0) + pg_fatal("could not write handoff signal file \"%s\": %m", sigpath); + check_ok(); + + /* + * Fast-stop the old primary; ShutdownXLOG() consumes the sentinel and + * emits the handoff. system() rather than exec_prog(): the lifecycle + * subcommand runs before make_outputdirs() has set log_opts.logdir, so + * exec_prog() would fail on a "(null)/..." log path. + */ + prep_status("Shutting down the old primary at the handoff point"); + snprintf(cmd, sizeof(cmd), "\"%s/pg_ctl\" -w -D \"%s\" -m fast stop", + old_cluster.bindir, old_cluster.pgdata); + rc = system(cmd); + if (rc != 0) + { + unlink(sigpath); + pg_fatal("could not shut down the old primary at the handoff point " + "(command \"%s\" returned %d)\n" + "Stop the old primary manually before running " + "\"pg_upgrade --wal-upgrade\".", + cmd, rc); + } + check_ok(); + + pg_log(PG_REPORT, + "\nHandoff trigger written to the old primary's WAL as it shut down.\n" + "The trigger propagates to streaming standbys through the normal\n" + "WAL/replication path, which replay it and shut down. Now run\n" + "\"pg_upgrade --wal-upgrade ...\"; then re-provision each standby\n" + "from the delivered upgrade window."); +} + +/* + * Dispatch a --wal-upgrade lifecycle subcommand and exit. Called from main() + * before the normal upgrade flow when user_opts.revertable_op is set. + */ +void +perform_revertable_op(void) +{ + switch (user_opts.revertable_op) + { + case REVERTABLE_OP_SIGNAL_HANDOFF: + do_signal_handoff(); + break; + case REVERTABLE_OP_NONE: + break; /* not reached */ + } +} diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c index 7da9dffe58..84f0928b7b 100644 --- a/src/bin/pg_upgrade/server.c +++ b/src/bin/pg_upgrade/server.c @@ -201,7 +201,58 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) * win on ext4. */ if (cluster == &new_cluster) - appendPQExpBufferStr(&pgoptions, " -c synchronous_commit=off -c fsync=off -c full_page_writes=off"); + { + /* + * Turn off durability requirements to improve object creation speed. + * For --wal-upgrade keep fsync and full_page_writes on: the smart + * shutdown checkpoint then flushes complete page images to disk, so + * all catalog pages are correct even with synchronous_commit off. + */ + if (user_opts.wal_upgrade) + { + /* keep fsync=on and full_page_writes=on for durability */ + appendPQExpBufferStr(&pgoptions, " -c synchronous_commit=off"); + + /* + * The end-of-upgrade window (CN checkpoint through + * PG_UPGRADE_COMPLETE) must survive intact in pg_wal/ for first + * startup and standby/PITR replay. Its full-page images are as + * large as the data (many GB, possibly TB), so checkpoints during + * the burst must not recycle any of it. + * + * The large max_wal_size / checkpoint_timeout below ensure no + * checkpoint fires between CN and shutdown, so nothing recycles + * the window during generation on any path. + * + * For the window to survive AFTER the burst server shuts down, a + * migrated physical slot pins it (see pg_upgrade.c), created + * before CN with immediately_reserve when the old cluster had a + * physical slot -- i.e. a standby was connected. KeepLogSeg() + * pins every segment at/after its restart_lsn, uncapped while + * max_slot_wal_keep_size is -1; set that here so the pin is + * size-independent. With no such slot the window is not pinned: + * on the archive-PITR path its durable home is the archive (post- + * shutdown recycling is harmless), and with neither a slot nor + * archiving --wal-upgrade has no window consumer at all, so it is + * recycled as ordinary WAL. + */ + appendPQExpBufferStr(&pgoptions, + " -c max_slot_wal_keep_size=-1" + " -c max_wal_size=1TB" + " -c checkpoint_timeout=1h"); + + /* + * Window archiving (--archive-command) is configured in the new + * cluster's postgresql.conf, not here: an archive_command's + * spaces and shell metacharacters do not survive pg_ctl's "-o" + * string, and postgresql.conf also makes the auto-served cluster + * archive its post-upgrade tail. See + * write_wal_upgrade_archive_conf(). + */ + } + else + appendPQExpBufferStr(&pgoptions, " -c synchronous_commit=off -c fsync=off -c full_page_writes=off"); + } /* * Use -b to disable autovacuum and logical replication launcher @@ -310,7 +361,6 @@ stop_postmaster(bool in_atexit) os_info.running_cluster = NULL; } - /* * check_pghost_envvar() * diff --git a/src/bin/pg_upgrade/t/009_initdb_option.pl b/src/bin/pg_upgrade/t/009_initdb_option.pl new file mode 100644 index 0000000000..772b78befc --- /dev/null +++ b/src/bin/pg_upgrade/t/009_initdb_option.pl @@ -0,0 +1,218 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the --initdb option of pg_upgrade: pg_upgrade creates the new cluster +# itself via initdb, instead of requiring the user to have run initdb first. + +use strict; +use warnings FATAL => 'all'; + +use File::Path qw(rmtree); +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize and populate the old cluster. +# +# Use non-default settings that --initdb must carry over to the new cluster +# (derived from the old cluster's pg_control): disabled data checksums (initdb +# enables them by default since PG18), a non-default WAL segment size, and the +# C locale. We check below that the new cluster inherits them. +my $oldnode = PostgreSQL::Test::Cluster->new('old_node'); +$oldnode->init( + extra => [ + '--no-data-checksums', + '--wal-segsize' => '2', + '--locale' => 'C', + ]); +$oldnode->start; +$oldnode->safe_psql('postgres', + "CREATE TABLE t (id int primary key, note text); " + . "INSERT INTO t SELECT g, 'row ' || g FROM generate_series(1, 100) g; " + . "CREATE DATABASE extra_db;"); +my $rows_before = + $oldnode->safe_psql('postgres', 'SELECT count(*) FROM t'); +is($rows_before, '100', 'old cluster has expected rows before upgrade'); + +# Record the old cluster's settings so we can compare them after the upgrade. +my $old_checksums = $oldnode->safe_psql('postgres', 'SHOW data_checksums'); +my $old_wal_segsize = $oldnode->safe_psql('postgres', 'SHOW wal_segment_size'); +my $old_encoding = $oldnode->safe_psql('postgres', + "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'"); +my $old_collate = $oldnode->safe_psql('postgres', + "SELECT datcollate FROM pg_database WHERE datname = 'template0'"); +my $old_ctype = $oldnode->safe_psql('postgres', + "SELECT datctype FROM pg_database WHERE datname = 'template0'"); +my $old_provider = $oldnode->safe_psql('postgres', + "SELECT datlocprovider FROM pg_database WHERE datname = 'template0'"); +$oldnode->stop; + +# Create the new node object but do NOT init() it: pg_upgrade --initdb is +# responsible for creating the data directory. Only new() runs, which +# allocates the port/host/basedir the framework needs. +my $newnode = PostgreSQL::Test::Cluster->new('new_node'); + +my $oldbindir = $oldnode->config_data('--bindir'); +my $newbindir = $newnode->config_data('--bindir'); + +# Sanity: the new data directory must not exist yet. +ok(!-d $newnode->data_dir, + 'new cluster data directory does not exist before --initdb'); + +# Run pg_upgrade with --initdb. We must run in a writable directory because +# pg_upgrade writes output files relative to the current directory. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + ], + 'run of pg_upgrade --initdb creates and upgrades the new cluster'); + +# The new data directory should now exist and be a v18+ cluster. +ok(-f $newnode->data_dir . '/PG_VERSION', + 'new cluster data directory created by --initdb'); + +# The framework's init() would normally write port/socket settings into +# postgresql.conf; since we skipped it, append them now so we can start the +# upgraded cluster through the test harness. Mirror init()'s own TCP vs Unix +# socket handling so this works on Windows (where TCP is used) as well. +my $host = $newnode->host; +$newnode->append_conf('postgresql.conf', "port = " . $newnode->port); +if ($PostgreSQL::Test::Cluster::use_tcp) +{ + $newnode->append_conf('postgresql.conf', "unix_socket_directories = ''"); + $newnode->append_conf('postgresql.conf', "listen_addresses = '$host'"); +} +else +{ + $newnode->append_conf('postgresql.conf', + "unix_socket_directories = '$host'"); + $newnode->append_conf('postgresql.conf', "listen_addresses = ''"); +} + +$newnode->start; + +# Verify the user data survived the upgrade. +my $rows_after = $newnode->safe_psql('postgres', 'SELECT count(*) FROM t'); +is($rows_after, '100', 'user data survived --initdb upgrade'); + +# Verify the extra database carried over too. +my $has_extra = $newnode->safe_psql('postgres', + "SELECT count(*) FROM pg_database WHERE datname = 'extra_db'"); +is($has_extra, '1', 'user database carried over by --initdb upgrade'); + +# Verify the new cluster is a newer major version than the old one. +my $newver = $newnode->safe_psql('postgres', + "SELECT current_setting('server_version_num')::int / 10000"); +ok($newver >= 18, "new cluster reports target major version ($newver)"); + +# --initdb must reproduce these settings from the old cluster; otherwise +# check_control_data() would reject the new cluster. Verify each carried over. +my $new_checksums = $newnode->safe_psql('postgres', 'SHOW data_checksums'); +is($new_checksums, $old_checksums, + "data_checksums propagated by --initdb ($new_checksums)"); + +my $new_wal_segsize = $newnode->safe_psql('postgres', 'SHOW wal_segment_size'); +is($new_wal_segsize, $old_wal_segsize, + "wal_segment_size propagated by --initdb ($new_wal_segsize)"); + +my $new_encoding = $newnode->safe_psql('postgres', + "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'"); +is($new_encoding, $old_encoding, + "template0 encoding propagated by --initdb ($new_encoding)"); + +my $new_collate = $newnode->safe_psql('postgres', + "SELECT datcollate FROM pg_database WHERE datname = 'template0'"); +is($new_collate, $old_collate, + "template0 collation propagated by --initdb ($new_collate)"); + +my $new_ctype = $newnode->safe_psql('postgres', + "SELECT datctype FROM pg_database WHERE datname = 'template0'"); +is($new_ctype, $old_ctype, + "template0 ctype propagated by --initdb ($new_ctype)"); + +my $new_provider = $newnode->safe_psql('postgres', + "SELECT datlocprovider FROM pg_database WHERE datname = 'template0'"); +is($new_provider, $old_provider, + "template0 locale provider propagated by --initdb ($new_provider)"); + +$newnode->stop; + +# --initdb must refuse to clobber an already-populated data directory, and the +# failure must come from pg_upgrade's own PG_VERSION check (not initdb's +# "directory not empty" error), so confirm the specific message. pg_upgrade +# prints its fatal message to stdout, so match there. +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + ], + 1, + [qr/already contains a database system/], + [qr/^$/], + '--initdb refuses to overwrite an existing cluster (PG_VERSION check)'); + +# --initdb must fail early with a clear message if initdb is not present in the +# new cluster's bin directory. Point --new-bindir at an empty directory and use +# a fresh (nonexistent) new data directory so we reach the initdb-present check. +my $empty_bindir = PostgreSQL::Test::Utils::tempdir; +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir . '_nonexistent', + '--old-bindir' => $oldbindir, + '--new-bindir' => $empty_bindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + ], + 1, + [qr/could not find "initdb"/], + [qr/^$/], + '--initdb fails early when initdb is missing from the new bindir'); + +# --initdb creates the new cluster, which --check (read-only) must not do, so +# the combination is rejected during option parsing. +command_checks_all( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir . '_nonexistent', + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + '--check', + ], + 1, + [qr/options -c\/--check and --initdb cannot be used together/], + [qr/^$/], + '--initdb and --check cannot be used together'); + +# NOTE: unlike upstream's standalone --initdb, this (--wal-upgrade) tree does +# NOT reject -O/--new-options with --initdb. create_new_cluster_via_initdb() +# derives initdb's options from the old cluster's control data and never +# forwards -O to initdb; -O reaches only the new cluster's postmaster, which the +# --wal-upgrade flow legitimately needs (e.g. -c allow_in_place_tablespaces=on). +# So there is no -O/--initdb rejection to test here. + +done_testing(); diff --git a/src/bin/pg_upgrade/t/010_wal_upgrade.pl b/src/bin/pg_upgrade/t/010_wal_upgrade.pl new file mode 100644 index 0000000000..cbd4c0f5e4 --- /dev/null +++ b/src/bin/pg_upgrade/t/010_wal_upgrade.pl @@ -0,0 +1,669 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Tests for "pg_upgrade --wal-upgrade" on a single (primary-only) cluster. +# +# --wal-upgrade captures the whole upgrade as WAL so the change is +# revertable, then leaves the new cluster as a normal, fully-upgraded cluster +# that comes up read-write on its first start like any upgraded cluster. This +# test drives the two primary-only paths and, at +# each step, asserts the cluster's control-file STATE transitions the way the +# state machine says it should and ends in the expected state: +# +# happy path: old (vN) --pg_upgrade--> new shut down cleanly, "in +# production" state, control checkpoint past the upgrade window +# --first start--> auto-serves, not in recovery, data preserved, +# target major version. +# +# crash path: old (vN) --pg_upgrade with COMPLETE suppressed--> a window +# with START but no COMPLETE on disk --first start--> FATAL +# (never serves a half-built catalog); the old cluster stays +# intact and startable, and the dead-end new cluster is discarded +# with rm -rf (there is no revert interface). +# +# Cross-version: when $ENV{oldinstall} is set the old cluster is built with an +# older major's binaries (checksums are pre-18 so pass -k there); otherwise the +# test runs same-version, exercising the state machine without a real gap. +# +# --------------------------------------------------------------------------- +# ASSUMPTIONS AND INVARIANTS --wal-upgrade relies on (why this works): +# +# 1. The window is self-contained and atomic. pg_upgrade writes the whole +# upgrade as a WAL "window": an end-of-upgrade checkpoint (CN), then +# XLOG_UPGRADE_START, the system-relation/SLRU/rawfile/dirtree images, and +# XLOG_UPGRADE_COMPLETE. Recovery anchors at CN and either reaches COMPLETE +# (fully upgraded) or it did not -- there is no half-upgraded steady state. +# +# 2. COMPLETE is the atomicity boundary. A window with START but no COMPLETE +# (crash mid-upgrade) must NEVER be served: the catalog is half-built and +# the cluster auto-serves read-write at end of recovery, so serving it would +# expose a corrupt catalog. First startup FATALs in that case. The durable +# pg_control upgrade_finalized flag (paired with a control checkpoint past CN) +# distinguishes a finished upgrade from a crashed partial one even if a torn +# final WAL page hides the COMPLETE record. +# +# 3. The old cluster stays intact on the copy-family transfer modes. With +# copy/clone/copy_file_range the old datadir's files are only read, so a +# failed or crashed upgrade can always fall back to starting the old cluster +# (the crash path below asserts this). (link/swap deliberately give that up, +# as in stock pg_upgrade.) +# +# 4. Counters ride in the CN checkpoint. The XID/OID/multixact counters are +# transplanted into pg_control before CN rather than WAL-logged separately, +# so the CN checkpoint record reproduces them on replay. + +use strict; +use warnings FATAL => 'all'; + +use File::Path qw(rmtree); +use File::Copy qw(copy); +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# The upgrade output files (delete_old_cluster.sh etc.) are written relative to +# the current directory, so run pg_upgrade from a writable scratch dir. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +# Read the "Database cluster state" line out of pg_controldata for $datadir, +# using $node's bindir so we read it with the matching-version tool. +sub cluster_state +{ + my ($node, $datadir) = @_; + my $bindir = $node->config_data('--bindir'); + my ($stdout, $stderr) = + run_command([ "$bindir/pg_controldata", '-D', $datadir ]); + return $1 if $stdout =~ /Database cluster state:\s+(.*)/; + return ''; +} + +# True if pg_control records the --wal-upgrade window as finalized (the durable +# flag that replaced the old pg_upgrade_complete.done marker file). +sub upgrade_finalized +{ + my ($node, $datadir) = @_; + my $bindir = $node->config_data('--bindir'); + my ($stdout, $stderr) = + run_command([ "$bindir/pg_controldata", '-D', $datadir ]); + return ($stdout =~ /wal-upgrade window finalized:\s+yes/) ? 1 : 0; +} + +# Build an old cluster and populate it with a little data whose survival we can +# check after the upgrade. Returns the $old node (stopped). +sub setup_old +{ + my ($name) = @_; + my $old = + PostgreSQL::Test::Cluster->new($name, install_path => $ENV{oldinstall}); + + # Checksums are enabled by default from v18 on, but not before, so pass + # '-k' on older installs so a checksum-on new cluster can be upgraded. + if (defined($ENV{oldinstall})) + { + $old->init(extra => ['-k']); + } + else + { + $old->init; + } + + $old->start; + $old->safe_psql( + 'postgres', qq{ + CREATE TABLE t (id int primary key, note text); + INSERT INTO t SELECT g, 'row ' || g FROM generate_series(1, 500) g; + CREATE TABLE toasted (id int, big text); + INSERT INTO toasted + SELECT g, repeat('abcdef0123456789', 2000) FROM generate_series(1, 200) g; + CREATE DATABASE extra_db; + }); + $old->stop; + return $old; +} + +# The arguments common to every pg_upgrade --wal-upgrade invocation here. +sub upgrade_cmd +{ + my ($old, $new, @extra) = @_; + return [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $old->config_data('--bindir'), + '--new-bindir' => $new->config_data('--bindir'), + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + '--initdb', + '--wal-upgrade', + @extra, + ]; +} + +# The framework's init() writes port/socket settings into postgresql.conf. A +# --initdb-created new cluster skipped init(), so append them before starting. +sub add_conn_conf +{ + my ($new) = @_; + my $conf = $new->data_dir . '/postgresql.conf'; + open(my $fh, '>>', $conf) or die "could not open $conf: $!"; + print $fh "\n# added by test to start the --initdb-created cluster\n"; + print $fh "port = " . $new->port . "\n"; + print $fh "listen_addresses = ''\n"; + print $fh "unix_socket_directories = '" . $new->host . "'\n"; + close($fh); +} + +# +# 1. HAPPY PATH: upgrade auto-serves and preserves data. +# +{ + my $old = setup_old('old_happy'); + # Do NOT init() the new node: --wal-upgrade --initdb creates it. + my $new = PostgreSQL::Test::Cluster->new('new_happy'); + + ok(!-d $new->data_dir, + 'happy: new data directory does not exist before --initdb'); + + command_ok(upgrade_cmd($old, $new), + 'happy: pg_upgrade --wal-upgrade --initdb succeeds'); + + # pg_upgrade shut the new cluster down cleanly, so its control file records + # a shut-down / in-production state, not a recovery/upgrade state. + my $state = cluster_state($new, $new->data_dir); + like($state, qr/^(shut down|in production)$/, + "happy: new cluster control state is shut-down/in-production ($state)"); + + # pg_control records the window as finalized once it reached COMPLETE. + ok(upgrade_finalized($new, $new->data_dir), + 'happy: pg_control upgrade_finalized set after a full upgrade'); + + # The new cluster comes up read-write on its first start, without re-replaying + # the window. + add_conn_conf($new); + $new->start; + + is($new->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 'f', 'happy: upgraded cluster is not in recovery'); + + # Data survived and the extra database carried over. + is($new->safe_psql('postgres', 'SELECT count(*) FROM t'), + '500', 'happy: user table data preserved'); + is($new->safe_psql('postgres', 'SELECT count(*) FROM toasted'), + '200', 'happy: toasted table data preserved'); + is( $new->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_database WHERE datname = 'extra_db'"), + '1', + 'happy: user database carried over'); + + # It reports the new major version. + my $newver = $new->safe_psql('postgres', + "SELECT current_setting('server_version_num')::int / 10000"); + ok($newver >= 18, "happy: new cluster reports target major ($newver)"); + + # A restart comes up the same way (starting an upgraded cluster is idempotent). + $new->restart; + is($new->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 'f', 'happy: still serving after restart'); + is($new->safe_psql('postgres', 'SELECT count(*) FROM t'), + '500', 'happy: data stable across restart'); + + # The durable pg_control upgrade_finalized flag persists across the restart. + # It lets a finalized cluster whose window tail is later torn or trimmed still + # be recognized as finalized rather than as a crashed mid-window upgrade. + ok(upgrade_finalized($new, $new->data_dir), + 'happy: durable upgrade_finalized flag set after restart'); + + $new->stop; + $old->clean_node; + $new->clean_node; +} + +# +# 1b. LINK MODE: window is still schema-only, and rollback is impossible. +# +# --link hardlinks user relations into the new cluster instead of copying them, +# and (as in stock pg_upgrade) disables the old cluster: global/pg_control is +# renamed to pg_control.old before the transfer, and once the new cluster starts +# the shared inodes make the old cluster unsafe to run. Two things must hold: +# +# (a) No user relation PAGES are WAL-logged. Regardless of transfer mode the +# window carries only pg_upgrade-rewritten SYSTEM files (as RELFILE_DATA +# full-page images) plus the RELINK manifest for user relations (identities +# only, no data); user data never rides the WAL. We scan the whole window +# with pg_waldump and assert the RELINK manifest is present and every +# UPGRADE_RELFILE_DATA record reports "0 user" entries. +# +# (b) Rollback to the old cluster is impossible: pg_control is gone (renamed to +# .old), so the old datadir will not start. +# +{ + my $old = setup_old('old_link'); + my $new = PostgreSQL::Test::Cluster->new('new_link'); + + # This subtest inspects the WINDOW's contents in the new cluster's pg_wal, + # so the window must be retained there. pg_upgrade retains it only when a + # physical slot is migrated (a standby is expected); with neither a slot nor + # archiving the window is unused and recycled. So give the old cluster a + # physical slot, which pg_upgrade migrates and uses to pin the window. + # (setup_old inits with wal_level=minimal; a slot needs >= replica.) + $old->append_conf('postgresql.conf', "wal_level = replica\n"); + $old->start; + $old->safe_psql('postgres', + "SELECT pg_create_physical_replication_slot('link_stby_slot', true)"); + $old->stop; + + # Fingerprint + control state of the intact old cluster, for contrast. + ok(-f $old->data_dir . '/global/pg_control', + 'link: old cluster has pg_control before the upgrade'); + + command_ok(upgrade_cmd($old, $new, '--link'), + 'link: pg_upgrade --wal-upgrade --initdb --link succeeds'); + + # (a) Scan the whole window in the new cluster's pg_wal. User relations must + # be represented ONLY by the RELINK manifest (identities, no data); their + # pages must never be WAL-logged. System relations DO travel, as one batched + # UPGRADE_RELFILE_DATA record of full-page images (the desc reports how many + # of its entries are user relations -- which must be 0). + # + # That RELFILE_DATA record is large (the whole system catalog) and can span a + # segment boundary, so we must dump the WHOLE pg_wal directory in one + # invocation (pg_waldump -p follows records across segments); a per-segment + # scan would miss a record that starts in one segment and ends in the next. + # pg_waldump errors at the end of available WAL -- that is expected, so read + # both stdout and stderr and parse whatever records it emitted. + my $bindir = $new->config_data('--bindir'); + my $waldir = $new->data_dir . '/pg_wal'; + # pg_waldump -p needs an explicit start LSN. Use the start of the lowest + # segment present so it dumps everything (following records across segment + # boundaries) to the end of available WAL. + opendir(my $wd, $waldir) or die "opendir $waldir: $!"; + my @segs = sort grep { /^[0-9A-F]{24}$/ } readdir $wd; + closedir $wd; + # Segment file is TTTTTTTT LLLLLLLL SSSSSSSS (timeline, log, seg); the byte + # offset of its first record is (log, seg*WalSegSz) i.e. LSN "log/seg000000". + my ($loghex, $seghex) = $segs[0] =~ /^[0-9A-F]{8}([0-9A-F]{8})([0-9A-F]{8})$/; + my $start = sprintf('%X/%s000028', hex($loghex), substr($seghex, 6, 2)); + my ($stdout, $stderr); + IPC::Run::run [ "$bindir/pg_waldump", '-p', $waldir, '-s', $start ], '>', + \$stdout, '2>', \$stderr; + my $dump = $stdout . $stderr; + my $saw_relink = ($dump =~ /UPGRADE_RELINK/); + my $saw_relfile = ($dump =~ /UPGRADE_RELFILE_DATA/); + my $saw_user_relfile = 0; + while ($dump =~ /UPGRADE_RELFILE_DATA\s+\d+ files \((\d+) user\)/g) + { + $saw_user_relfile = 1 if $1 > 0; + } + ok($saw_relink, 'link: window carries the RELINK manifest for user relations'); + ok($saw_relfile, + 'link: window carries RELFILE_DATA (system relation full-page images)'); + is($saw_user_relfile, 0, + 'link: no user relation pages are WAL-logged (window is schema-only)'); + + # (b) The old cluster is disabled: pg_control renamed to .old. + ok(!-f $old->data_dir . '/global/pg_control', + 'link: old cluster pg_control removed (renamed to .old)'); + ok(-f $old->data_dir . '/global/pg_control.old', + 'link: old cluster pg_control saved as .old'); + + # Bring the new cluster up (this is what makes the old cluster unsafe); the + # old datadir then can no longer be started. + add_conn_conf($new); + $new->start; + is($new->safe_psql('postgres', 'SELECT count(*) FROM t'), + '500', 'link: user data present on the new cluster (hardlinked in)'); + + my $oldbin = $old->config_data('--bindir'); + my $ret = run_log( + [ "$oldbin/pg_ctl", '-D', $old->data_dir, '-w', '-t', '10', 'start' ]); + ok(!$ret, 'link: old cluster refuses to start (rollback impossible)'); + # If it somehow started, stop it so it does not leak into later tests. + run_log([ "$oldbin/pg_ctl", '-D', $old->data_dir, 'stop' ]) if $ret; + + $new->stop; + $old->clean_node; + $new->clean_node; +} + +# +# 2. CRASH PATH: a window with START but no COMPLETE must be refused. +# +# PG_UPGRADE_TEST_SKIP_COMPLETE (a test-only fault-injection hook) makes +# pg_upgrade emit the whole upgrade window but omit the COMPLETE marker, +# simulating a crash after START. The new cluster must FATAL on first start +# (never serve a half-built catalog) and the old cluster must stay intact. The +# dead-end new cluster carries no state worth keeping (there is no revert +# interface), so it is simply discarded with rm -rf. +# +# The hook is gated on USE_ASSERT_CHECKING (a production build ignores the env +# var and can never emit an incomplete window), so this section only runs on a +# cassert build. +# +SKIP: +{ + skip 'crash-path fault injection requires a cassert build', 6 + unless check_pg_config(qr/^#define USE_ASSERT_CHECKING 1/); + + my $old = setup_old('old_crash'); + + # Fingerprint the old cluster so the post-crash check can confirm it is + # undamaged. + $old->start; + my $old_fp = + $old->safe_psql('postgres', 'SELECT count(*), sum(id) FROM t'); + $old->stop; + + my $new = PostgreSQL::Test::Cluster->new('new_crash'); + + # Upgrade with COMPLETE suppressed. pg_upgrade itself still succeeds; the + # missing COMPLETE only bites at the new cluster's first start. + { + local $ENV{PG_UPGRADE_TEST_SKIP_COMPLETE} = '1'; + command_ok(upgrade_cmd($old, $new), + 'crash: pg_upgrade succeeds even with COMPLETE suppressed'); + } + + # A partial window never sets the finalized flag; that absence is what marks + # the cluster as not finalized. + ok(!upgrade_finalized($new, $new->data_dir), + 'crash: upgrade_finalized flag unset on a partial window'); + + # First start must FAIL (fail_ok so we can assert on it instead of bailing). + add_conn_conf($new); + my $started = $new->start(fail_ok => 1); + is($started, 0, 'crash: new cluster refuses to start on a partial window'); + + # And it must FATAL specifically because the window is incomplete -- not + # for some unrelated reason. + my $log = slurp_file($new->logfile); + like( + $log, + qr/pg_upgrade WAL is incomplete|found START without COMPLETE/, + 'crash: startup FATALed with the "incomplete window" message'); + + # The old cluster is untouched: it starts and its data is identical. + $old->start; + is($old->safe_psql('postgres', 'SELECT count(*), sum(id) FROM t'), + $old_fp, 'crash: old cluster intact and startable after the failed upgrade'); + $old->stop; + + # The dead-end new cluster is simply discarded (rm -rf); there is no revert + # interface, and the old cluster is the source of truth. + rmtree($new->data_dir); + ok(!-d $new->data_dir, + 'crash: half-upgraded new data directory discarded'); + + $old->clean_node; + $new->clean_node; +} + +# +# 3. BACKUP-GAP / PITR PATH: recover the transactions executed after the +# upgrade went live from a pre-upgrade base backup + archived WAL, ACROSS +# the upgrade boundary, WITHOUT a new base backup and WITHOUT a standby. +# +# The operator runs a single primary with a periodic base backup and continuous +# WAL archiving. The upgrade procedure is: +# +# 1. stop the old server +# 2. pg_upgrade --wal-upgrade +# 3. start the new server +# 4. the application connects and executes transactions <-- must survive +# 5. take a new base backup +# +# If the storage is lost after step 4 but before step 5, upstream pg_upgrade +# loses the step-4 transactions: the pre-upgrade base backup cannot be rolled +# forward across the version boundary, and no post-upgrade base backup exists +# yet. On a large database step 5 can take a long time, so that gap is wide. +# +# With --wal-upgrade the upgrade itself is WAL, so the claim is: restore the +# last pre-upgrade base backup, replay archived WAL up to the upgrade, replay +# the upgrade window, then keep replaying the WAL generated on the new version +# -- recovering the step-4 transactions with no new base backup. This test +# encodes that workflow end to end. +{ + # A single archive both the old and the new cluster feed, and that the + # PITR restore reads back -- one continuous WAL history on disk. + my $archive = "${PostgreSQL::Test::Utils::tmp_check}/pitr_archive"; + rmtree($archive); + mkdir($archive) or die "could not create $archive: $!"; + # Archive atomically (copy to a temp name, then rename) so an immediate + # stop that kills the archiver mid-copy can never leave a truncated segment + # in the archive for the PITR restore to trip over. + my $arch_cmd = + "cp \"%p\" \"$archive/%f.tmp\" && mv \"$archive/%f.tmp\" \"$archive/%f\""; + my $restore_cmd = "cp \"$archive/%f\" \"%p\""; + + # Old cluster with WAL archiving on. + my $old = + PostgreSQL::Test::Cluster->new('old_pitr', install_path => $ENV{oldinstall}); + if (defined($ENV{oldinstall})) + { + $old->init(extra => ['-k'], allows_streaming => 1); + } + else + { + $old->init(allows_streaming => 1); + } + $old->append_conf('postgresql.conf', + "archive_mode = on\narchive_command = '$arch_cmd'\n"); + $old->start; + + # Pre-upgrade data. + $old->safe_psql( + 'postgres', qq{ + CREATE TABLE t (id int primary key, note text); + INSERT INTO t SELECT g, 'pre ' || g FROM generate_series(1, 500) g; + }); + + # Step 0: the periodic pre-upgrade base backup the operator already has. + $old->backup('pitr_base'); + + # Capture the old cluster's final WAL position. Phase 1 of recovery stops + # here: at the boundary, past all pre-upgrade transactions, before the + # upgrade window (which lives at a later, non-contiguous segment). + my $old_end_lsn = + $old->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); + + $old->stop; # step 1 + + # Step 2: pg_upgrade --wal-upgrade --initdb creates the new cluster. + my $new = PostgreSQL::Test::Cluster->new('new_pitr'); + command_ok(upgrade_cmd($old, $new), + 'pitr: pg_upgrade --wal-upgrade --initdb succeeds'); + + # The upgraded cluster inherits the old cluster's archive_command + # automatically (pg_upgrade --wal-upgrade carries it forward), so the upgrade + # window and the post-upgrade WAL flow to the SAME archive with no extra + # option. Verify that landed in the new cluster's config. + add_conn_conf($new); + my $newconf = slurp_file($new->data_dir . '/postgresql.conf'); + like($newconf, qr/archive_mode\s*=\s*on/, + 'pitr: upgraded cluster inherited archive_mode'); + + # The upgrade window (CN..COMPLETE) must have reached the archive; the burst + # server's wait-for-archive barrier guarantees this. Confirm the window + # segments are present by scanning the archive for the START/COMPLETE records. + my $bindir = $new->config_data('--bindir'); + my ($start_seg, $complete_seg) = ('', ''); + opendir(my $ad, $archive) or die "opendir $archive: $!"; + for my $f (sort grep { /^[0-9A-F]{24}$/ } readdir $ad) + { + my ($out) = run_command([ "$bindir/pg_waldump", "$archive/$f" ]); + $start_seg = $f if $out =~ /PG_UPGRADE_START/; + $complete_seg = $f if $out =~ /PG_UPGRADE_COMPLETE/; + } + closedir $ad; + ok($start_seg ne '' && $complete_seg ne '', + "pitr: upgrade window archived (START in $start_seg, COMPLETE in $complete_seg)"); + + # Step 3: start the upgraded cluster (auto-serves), archiving its tail. + $new->start; + + # On the backup/PITR path the window's durable home is the archive, so no + # retention slot is created at all (the wait-for-archive barrier guarantees + # the window is archived before shutdown; a slot would only pin pg_wal/ + # forever with no standby to consume it). Confirm no physical slot exists. + is($new->safe_psql('postgres', + "SELECT count(*) FROM pg_replication_slots WHERE slot_type = 'physical'"), + '0', 'pitr: no retention slot on the archive path'); + + # Step 4: the application executes transactions on the new version. These + # are the ones the backup gap loses upstream. + $new->safe_psql('postgres', + "INSERT INTO t SELECT g, 'post ' || g FROM generate_series(501, 800) g;" + ); + $new->safe_psql('postgres', + 'CREATE TABLE only_on_new (x int); INSERT INTO only_on_new VALUES (42);'); + + # Push the step-4 WAL out to the archive (switch + checkpoint + switch), the + # way continuous archiving would before the operator got to step 5. + $new->safe_psql('postgres', + 'SELECT pg_switch_wal(); CHECKPOINT; SELECT pg_switch_wal();'); + # Give the archiver a moment to drain the tail. + $new->poll_query_until('postgres', + "SELECT last_archived_wal IS NOT NULL FROM pg_stat_archiver"); + + # DISASTER: storage lost after step 4, before the step-5 base backup. The + # new cluster's data dir (and its local pg_wal upgrade window) are gone; only + # the pre-upgrade base backup and the archive survive off host. + $new->stop('immediate'); + my $new_datadir = $new->data_dir; + rmtree($new_datadir); + ok(!-d $new_datadir, 'pitr: upgraded cluster storage lost before new backup'); + + # ---- RECOVERY: two-phase PITR across the upgrade boundary ---- + # + # Phase 1 (OLD binary): restore the pre-upgrade base backup and replay + # archived OLD WAL up to the upgrade boundary, stopping there WITHOUT + # promoting (a promotion would fork a new timeline and clobber the state + # Phase 2 needs). This recovers every pre-upgrade transaction, including + # those written after the base backup. + # Same-version test: old and new binaries are identical, so one node drives + # both phases. For a real cross-version run ($ENV{oldinstall} set), Phase 1 + # must use the OLD binary (to read the old-version restored backup) and Phase + # 2 the NEW binary (to replay the window). The restore node is a NEW-version + # node so its start()/psql use the new binaries; Phase 1 is driven with the + # OLD bindir explicitly via raw pg_ctl. + my $old_bindir = $old->config_data('--bindir'); + my $restore = PostgreSQL::Test::Cluster->new('restore_pitr'); + $restore->init_from_backup($old, 'pitr_base'); + + # The boundary is the old cluster's final WAL position, captured above. + # Stopping there keeps Phase 1 on timeline 1, past every pre-upgrade + # transaction, without entering the (non-contiguous) upgrade window. + $restore->append_conf('postgresql.conf', + "restore_command = '$restore_cmd'\n" + . "recovery_target_lsn = '$old_end_lsn'\n" + . "recovery_target_inclusive = on\n" + . "recovery_target_action = 'shutdown'\n"); + open(my $s1, '>', $restore->data_dir . '/recovery.signal') or die $!; + close($s1); + + # Phase 1 runs the OLD binary and stops itself at the recovery target + # (recovery_target_action=shutdown). Drive it with a raw pg_ctl rather than + # $node->start, because the node framework expects start() to leave a running + # server with a live postmaster.pid; a self-terminating recovery would leave + # its pid bookkeeping inconsistent and bail the whole test. + # pg_ctl --wait returns non-zero when the server exits instead of staying up + # (which is exactly what recovery_target_action=shutdown does), so ignore the + # exit code and assert on the log instead. + my $p1log = "${PostgreSQL::Test::Utils::tmp_check}/pitr_phase1.log"; + PostgreSQL::Test::Utils::system_log( + "$old_bindir/pg_ctl", '--wait', + '--pgdata' => $restore->data_dir, + '--log' => $p1log, + '--options' => "--cluster-name=restore_pitr_p1", + 'start'); + like(slurp_file($p1log), qr/shutdown at recovery target/, + 'pitr phase 1: old binary stopped at the upgrade boundary (no promote)'); + + # Phase 2 (NEW binary): stage the window segments (CN..COMPLETE) into pg_wal/ + # so the new binary's local scan re-anchors recovery at CN, then replay the + # window + the archived post-upgrade tail. No recovery_target this time. + # + # This copy mirrors a real operator's recovery step, not a test shortcut: on + # the archive-PITR path the window-detection scan (PerformWalUpgradeIfNeeded) + # reads the local pg_wal/ before archive recovery is wired up, so the window + # segments must physically be present there when the new binary starts -- they + # cannot yet be pulled from the archive via restore_command at that point (the + # post-upgrade tail, which IS fetched by restore_command, still is). Fetching + # the window from the archive too is future work (move upgrade detection after + # archive-recovery init). The streaming-standby path (011) needs no such + # staging: it receives the window over replication. + my $rwal = $restore->data_dir . '/pg_wal'; + opendir(my $ad2, $archive) or die $!; + for my $f (sort grep { /^[0-9A-F]{24}$/ } readdir $ad2) + { + next if $f lt $start_seg or $f gt $complete_seg; + copy("$archive/$f", "$rwal/$f") or die "stage $f: $!"; + } + closedir $ad2; + + # Rewrite recovery config: new binary, restore_command for the tail, follow + # to the latest timeline, and crucially NO recovery_target (a stale target + # before CN would abort as "stop point before consistent recovery point"). + my $conf = $restore->data_dir . '/postgresql.conf'; + # Strip the phase-1 recovery_target_* lines, keep base settings. + my $txt = slurp_file($conf); + $txt =~ s/^recovery_target.*\n//mg; + open(my $cw, '>', $conf) or die $!; + print $cw $txt; + print $cw "recovery_target_timeline = 'latest'\n"; + # A PITR/standby replay requires the recovery node's resource GUCs to be at + # least the primary's (a PARAMETER_CHANGE record in the window carries the + # primary's values, e.g. max_connections=100). The no-initdb synthesized + # pg_control comes up with this binary's defaults (max_connections=10), which + # are lower, so recovery would abort ("insufficient parameter settings"). + # Match the primary's values, exactly as an operator provisioning a restore + # node must. + print $cw "max_connections = 100\n"; + print $cw "max_worker_processes = 8\n"; + print $cw "max_wal_senders = 10\n"; + print $cw "max_prepared_transactions = 0\n"; + print $cw "max_locks_per_transaction = 128\n"; + close($cw); + open(my $s2, '>', $restore->data_dir . '/recovery.signal') or die $!; + close($s2); + + # Cross-version: Phase 1 left an OLD-version pg_control that this NEW binary + # would reject at the version gate. Drop the pg_upgrade.signal sentinel next + # to the staged window so the new binary, seeing recovery.signal + this + # sentinel in checkDataDir(), synthesizes a new-version pg_control before the + # gate. (recovery.signal, not primary_conninfo, so this is the archive path.) + # Same-version needs no synthesis and is unaffected (control file already + # matches), but the sentinel is harmless there. + open(my $u2, '>', $restore->data_dir . '/pg_upgrade.signal') or die $!; + close($u2); + + my $p2 = $restore->start(fail_ok => 1); + is($p2, 1, 'pitr phase 2: new binary replays the window + tail and comes up'); + + SKIP: + { + skip 'phase 2 did not start', 3 if $p2 != 1; + + # The server accepts read-only connections at consistency, but recovery + # is still replaying the window + tail. Wait for recovery to finish + # (promotion) before checking data, else we observe a mid-replay state. + $restore->poll_query_until('postgres', 'SELECT NOT pg_is_in_recovery()') + or die 'phase 2 recovery did not finish'; + + # The step-4 transactions survived without a new base backup. + is($restore->safe_psql('postgres', 'SELECT count(*) FROM t'), + '800', 'pitr: pre- and post-upgrade rows both recovered'); + is( $restore->safe_psql( + 'postgres', "SELECT to_regclass('only_on_new') IS NOT NULL"), + 't', 'pitr: post-upgrade DDL recovered'); + is($restore->safe_psql('postgres', 'SELECT x FROM only_on_new'), + '42', 'pitr: post-upgrade row recovered'); + + $restore->stop; + } + + $old->clean_node; + $restore->clean_node; +} + +done_testing(); diff --git a/src/bin/pg_upgrade/t/011_wal_upgrade_standby.pl b/src/bin/pg_upgrade/t/011_wal_upgrade_standby.pl new file mode 100644 index 0000000000..99d82add43 --- /dev/null +++ b/src/bin/pg_upgrade/t/011_wal_upgrade_standby.pl @@ -0,0 +1,753 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Tests for "pg_upgrade --wal-upgrade" delivering the upgrade to a physical +# standby by STREAMING the upgrade window from the live, upgraded primary. +# +# Under --wal-upgrade the whole upgrade is captured as WAL (the CN..COMPLETE +# "window"), and a migrated physical replication slot pins that window in the +# primary's pg_wal/ so it survives the upgrade and stays streamable. A new-version +# skeleton that points primary_conninfo at the upgraded primary DERIVES the window +# anchor (CN) LOCALLY from its retained old data directory (reproducing +# pg_resetwal's byte-contiguous placement), taking only the system identifier from +# the primary's standard IDENTIFY_SYSTEM, arms its control file at CN, and streams +# the window forward -- becoming a hot standby that serves the upgraded data. No +# operator "prepare" step, no hand-copied WAL, and no bespoke replication command +# are required. +# +# --------------------------------------------------------------------------- +# ASSUMPTIONS AND INVARIANTS the standby path relies on (why this works): +# +# 1. Relfilenode/OID identity (holds for ALL transfer modes). pg_upgrade +# preserves every user relation's relfilenumber and its database/tablespace +# OIDs; the transfer step uses that same number for the file on both sides +# and the mode (copy/clone/copy_file_range/link/swap) changes only HOW the +# file is moved, never its name. So a user relation has the SAME on-disk +# path in the old cluster and the upgraded new cluster. This identity is +# what lets the standby place a user file at the identical relative path +# from the old datadir -- no catalog lookup -- however the mode places it +# (copy/clone/copy_file_range/link/swap; see invariant 3). +# +# 2. Schema-only window. The streamed window carries only what pg_upgrade +# rewrote: system catalogs, SLRU, the directory skeleton, pg_filenode.map / +# PG_VERSION -- NOT user relations (which pg_upgrade transfers unchanged). +# The window is therefore schema-sized, not data-sized. Instead the window +# includes an XLOG_UPGRADE_RELINK manifest naming exactly the user files it +# omitted (emitted from the same capture walk, so the two cannot drift). +# +# 3. User relations come from the standby's OWN retained old datadir. A +# streaming standby is a new-version skeleton -- either a fresh initdb or a +# bare data dir whose new-version pg_control/PG_VERSION are synthesized at +# startup (SynthesizeUpgradeStreamControlFile; initdb is optional) -- plus an +# empty "pg_upgrade.signal" sentinel; its retained pre-upgrade data directory +# is named by the pg_upgrade_standby_old_datadir GUC in postgresql.conf. Redo of the +# RELINK manifest processes it one relation-file segment at a time, placing +# each from the old datadir at the identical relative path in the skeleton. +# By default (pg_upgrade_standby_transfer_mode = mirror) it reproduces the primary's +# transfer mode, but the standby MAY choose a different mode via that GUC: +# - copy: an independent whole-file copy (copy_file()); 2x space. +# - clone: a reflink / copy-on-write clone (copyfile(COPYFILE_CLONE_FORCE) +# on macOS, ioctl(FICLONE) on Linux) -- near-zero extra space, exactly as +# pg_upgrade --clone does on the primary. +# - copy_file_range: copy_file_range(), a server-side/reflink copy. +# The redo reproduces each mode's RESULT with the same primitive +# pg_upgrade uses; if a reflink mode's filesystem can't reflink, redo +# FATALs rather than silently fall back to a 2x copy. In all three the +# old file is only read, so the old datadir stays a bootable rollback +# target. +# - link: a per-file hardlink (link()), sharing the old inode. +# - swap: a rename(), MOVING the file out of the old datadir into the +# skeleton -- upstream --swap renames whole database directories, and a +# per-file rename here reproduces that (no entry left in the old datadir, +# unlike --link). +# Both link and swap are fast and space-free and intentionally sacrifice +# the old datadir -- the tradeoff the operator accepted by choosing +# --link/--swap on the primary (which disables its own old cluster). +# The primary can't know the standby's old-datadir path, so it is supplied +# locally via pg_upgrade_standby_old_datadir; nothing new is streamed for it. +# +# 4. Recovery anchors at CN. Recovery starts at the end-of-upgrade checkpoint +# (CN); the XID/OID/multixact counters were transplanted into pg_control +# before CN, so the CN checkpoint record carries them. The skeleton fetches +# CN (sysid + LSN + redo + TLI) from the primary over the replication +# connection and arms its control file at CN before replay. +# +# 5. WAL page formats differ across majors. The new-format window is +# unreadable by an old-version standby; conversely a fresh skeleton must be +# the NEW (in-tree) version. This is why an existing old standby cannot just +# follow the upgrade: it pauses at the handoff (below) -- staying up to serve +# reads -- and is replaced by a new-version skeleton that streams the window. +# +# This test drives that path and, at each step, asserts the state transitions +# land where the state machine says they should: +# +# handoff: an EXISTING old-version standby streaming from the old primary, +# on --wal-upgrade-signal-handoff, replays XLOG_UPGRADE_HANDOFF and +# PAUSES recovery at the boundary (did not promote or shut down), +# staying up to serve read-only queries while a new-version skeleton +# is provisioned. It is then stopped and its retained datadir +# becomes the relink source for the skeleton below. Same-version +# only: the handoff record exists solely in the patched binary. +# abort: the handoff pause is reversible -- if the upgrade is abandoned, the +# old primary is restarted read-write and the standby resumes replay +# past the (no-op) handoff record and keeps following it. +# primary: old (vN) --pg_upgrade--> auto-served new primary (not in +# recovery), retention slot present, window pinned in pg_wal/. +# standby: a fresh new-version skeleton + primary_conninfo + +# pg_upgrade_standby_old_datadir + an empty pg_upgrade.signal sentinel +# --start--> auto-armed from the primary, STREAMED the window, and +# the RELINK redo copied the user relations in; came up in recovery +# (pg_is_in_recovery = t), converged to the primary's data, and the +# copies are inode-independent (old datadir left intact). +# no-initdb: the same, but from a BARE data dir (config + sentinel only, never +# initdb'd) -- checkDataDir() synthesizes pg_control + PG_VERSION from +# this binary's constants, so no initdb is required to stream. +# negative: streaming the window into a skeleton with pg_upgrade_standby_old_datadir +# unset leaves the user relations ABSENT -- the window alone does not +# reconstruct user data, proving the relink step is load-bearing. +# And a fresh cluster with no primary and no window is just an +# ordinary empty cluster (no silent alternative delivery path). +# +# Cross-version: when $ENV{oldinstall} is set the old primary is built with an +# older major's binaries; otherwise the test runs same-version. Either way the +# STANDBY skeleton is always the new (in-tree) version, since streaming a window +# into an older-version standby is not the supported direction. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# pg_upgrade writes output files relative to the current directory. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +# Config that lets a node act as a streaming-replication primary. +# Use values >= what PostgreSQL::Test::Cluster's allows_streaming default sets +# (max_wal_senders = 10), so a hot standby replaying this primary's WAL does not +# abort with "insufficient parameter settings". +my $primary_conf = q{ +wal_level = replica +max_wal_senders = 10 +max_replication_slots = 10 +hot_standby = on +}; + +# +# 1. Old primary with data -> upgrade -> auto-served new primary. +# +my $old = + PostgreSQL::Test::Cluster->new('old', install_path => $ENV{oldinstall}); +if (defined($ENV{oldinstall})) +{ + # Checksums default on from v18; pass -k on older installs so a checksum-on + # new cluster can be upgraded. + $old->init(allows_streaming => 1, extra => ['-k']); +} +else +{ + $old->init(allows_streaming => 1); +} +$old->append_conf('postgresql.conf', $primary_conf); +$old->start; +$old->safe_psql( + 'postgres', qq{ + CREATE TABLE t (id int primary key, v text); + INSERT INTO t SELECT g, 'v' || g FROM generate_series(1, 2000) g; + CREATE INDEX ON t (v); + CREATE TABLE toasted (id int, big text); + INSERT INTO toasted + SELECT g, repeat('abcdef0123456789', 3000) FROM generate_series(1, 300) g; + -- Large objects live in pg_largeobject[_metadata], which pg_upgrade + -- transfers verbatim as user data. Under --wal-upgrade those catalogs are + -- excluded from the window and named in the RELINK manifest instead, so the + -- standby links them from its own retained old datadir like any user + -- relation; seed some to exercise that path. + SELECT lo_from_bytea(0, decode(repeat(md5(g::text), 50), 'hex')) + FROM generate_series(1, 40) g; +}); +# Fold the large-object content into the fingerprint so a mis-delivered +# pg_largeobject on the standby (from WAL vs. relink) is caught by convergence. +my $fp_query = q{SELECT count(*), sum(hashtext(v)::bigint), + (SELECT count(*) || ':' || coalesce(sum(length(data))::text, '0') + FROM pg_largeobject) FROM t}; +my $want = $old->safe_psql('postgres', $fp_query); + +# Create a NAMED physical replication slot on the old primary. Under +# --wal-upgrade pg_upgrade migrates physical slots (stock pg_upgrade migrates +# only logical ones), so this slot must reappear on the upgraded primary under +# the SAME name -- preserving the standby's slot identity across the upgrade. +# (Only exercised same-version; a stock old major has no --wal-upgrade +# machinery, but the slot itself is ordinary and would migrate the same way.) +my $migrated_slot = 'my_standby_slot'; +if (!defined($ENV{oldinstall})) +{ + $old->safe_psql('postgres', + "SELECT pg_create_physical_replication_slot('$migrated_slot', true)"); +} + +# +# 1a. Handoff: an EXISTING old-version standby pauses at the upgrade point. +# +# --wal-upgrade-signal-handoff writes an XLOG_UPGRADE_HANDOFF record into the +# LIVE old primary's WAL and fast-stops the primary at that point. A streaming +# old standby replays the record and PAUSES recovery at the boundary (it does not +# shut down or promote): it cannot follow the upgrade in the old WAL format, so it +# stays up serving read-only queries while a fresh new-version standby (section 2) +# is provisioned to take over by streaming the window. The pause is reversible +# (see section 2b for the aborted-upgrade case). +# +# The handoff record (emitted by the old primary's own ShutdownXLOG) exists +# only in this patched binary, so this can only be exercised same-version; a +# stock old major cannot emit it. Skip it (but still stop the primary) when +# running cross-version. +# The paused old standby is then stopped, and its retained datadir becomes the +# RELINK SOURCE for a fresh new-version skeleton in section 2 (it is never +# adopted/started in place -- the skeleton is a separate initdb'd cluster that +# links user relations from it), so declare it at outer scope (only populated on +# the same-version path, where the handoff can run). +my $oldsby; + +if (!defined($ENV{oldinstall})) +{ + # A real old-version standby streaming from the live old primary. + $old->backup('handoff_base'); + $oldsby = PostgreSQL::Test::Cluster->new('old_standby'); + $oldsby->init_from_backup($old, 'handoff_base', has_streaming => 1); + $oldsby->append_conf('postgresql.conf', $primary_conf); + $oldsby->start; + + # It is a hot standby that has converged to the primary's data. + $old->wait_for_catchup($oldsby, 'replay', $old->lsn('insert')); + is($oldsby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 't', 'handoff: old standby is a hot standby before the handoff'); + is($oldsby->safe_psql('postgres', 'SELECT count(*) FROM t'), + '2000', 'handoff: old standby has the pre-upgrade data'); + + # Record the current end of the standby's log so wait_for_log() below only + # matches the handoff message emitted from this point on. + my $logstart = -s $oldsby->logfile; + + # Signal the handoff: writes the trigger into the running old primary's WAL + # and fast-stops the primary at that point. + command_ok( + [ + 'pg_upgrade', '--no-sync', + '--wal-upgrade-signal-handoff', + '--old-datadir' => $old->data_dir, + '--old-bindir' => $old->config_data('--bindir'), + '--socketdir' => $old->host, + '--old-port' => $old->port, + ], + 'handoff: signal-handoff writes the trigger and stops the old primary'); + + # pg_upgrade shut the old primary down itself; sync the framework's state. + $old->_update_pid(0); + + # The old standby replays the handoff and PAUSES recovery at the boundary + # (rather than shutting down): it cannot follow the upgrade in the old WAL + # format, but it stays up serving read-only queries while a fresh + # new-version skeleton is provisioned. Wait for the pause message. + $oldsby->wait_for_log( + qr/reached pg_upgrade handoff on standby; pausing recovery at the upgrade boundary/, + $logstart); + ok(1, 'handoff: old standby replayed the handoff and paused at the boundary'); + + # It paused, it did not shut down or promote: the postmaster is still up and + # still in recovery, and it still answers read-only queries at the + # pre-upgrade snapshot. This is the read-only-availability guarantee: the + # standby keeps serving through the upgrade instead of going dark. + ok(-f $oldsby->data_dir . '/postmaster.pid', + 'handoff: old standby is still running after the handoff (paused, not stopped)'); + is($oldsby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 't', 'handoff: paused standby is still in recovery (did not promote)'); + is($oldsby->safe_psql('postgres', 'SELECT count(*) FROM t'), + '2000', 'handoff: paused standby still serves the pre-upgrade data read-only'); + + # Retire the paused standby in favor of the new skeleton built in section 2. + # A clean stop leaves its data directory a valid, bootable vN cluster; the + # skeleton's RELINK redo (default copy mode) only READS these files, so the + # old datadir stays intact both as the relink source and as a rollback + # target. + $oldsby->stop; +} +else +{ + $old->stop; +} + +# The new primary is created by --initdb; do NOT init() it here. +my $new = PostgreSQL::Test::Cluster->new('new'); + +command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $old->config_data('--bindir'), + '--new-bindir' => $new->config_data('--bindir'), + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + '--initdb', + '--wal-upgrade', + ], + 'primary: pg_upgrade --wal-upgrade --initdb succeeds'); + +# The --initdb-created new cluster skipped init(); append the settings the test +# harness needs to start and stream from it. +my $conf = $new->data_dir . '/postgresql.conf'; +open(my $fh, '>>', $conf) or die "could not open $conf: $!"; +print $fh "\n# added by test\n"; +print $fh "port = " . $new->port . "\n"; +print $fh "listen_addresses = '" + . ($PostgreSQL::Test::Utils::windows_os ? '127.0.0.1' : '') . "'\n"; +print $fh "unix_socket_directories = '" . $new->host . "'\n"; +print $fh $primary_conf; +close($fh); + +# Trust replication + normal connections locally so the standby can connect. +$new->append_conf('pg_hba.conf', + "local replication all trust\nhost replication all 127.0.0.1/32 trust\nhost replication all ::1/128 trust\n" +); + +$new->start; + +# The upgraded primary comes up read-write (not in recovery) with its data. +is($new->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 'f', 'primary: serving read-write, not in recovery'); +is($new->safe_psql('postgres', $fp_query), + $want, 'primary: data preserved after upgrade'); + +# The retention slot that pins the upgrade window is present on the live +# primary, so the window is streamable to a standby. +my $nslots = $new->safe_psql('postgres', + "SELECT count(*) FROM pg_replication_slots WHERE slot_type = 'physical'"); +ok($nslots >= 1, "primary: retention slot present ($nslots physical slot(s))"); + +# Physical-slot migration and reuse: the named slot created on the old primary +# must have been recreated on the upgraded primary under the same name, and -- +# because a migrated slot already pins the window -- the dedicated +# UPGRADE_WINDOW_SLOT ("pg_upgrade_window") must NOT have been created. So the +# migrated slot is reused as the window pin (only checked same-version, where +# the old cluster actually had a physical slot). +if (!defined($ENV{oldinstall})) +{ + is( $new->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_replication_slots " + . "WHERE slot_type = 'physical' AND slot_name = '$migrated_slot'"), + '1', + "primary: physical slot \"$migrated_slot\" migrated across the upgrade"); + + is( $new->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_replication_slots " + . "WHERE slot_name = 'pg_upgrade_window'"), + '0', + "primary: dedicated retention slot not created when a physical slot is reused"); +} + +# +# 2. Fresh skeleton streams the window; the RELINK manifest links user data. +# +# The upgrade window carries only the files pg_upgrade touched (system catalogs, +# SLRU, skeleton) -- NOT user relations, so it is schema-sized, not data-sized. +# Instead the window includes an XLOG_UPGRADE_RELINK manifest naming the user +# relation files pg_upgrade transferred; on redo the standby places each from its +# own retained old datadir into the fresh skeleton. With pg_upgrade_standby_transfer_mode +# left at the default (mirror) it reproduces the primary's mode; the primary here +# used --copy, so each is an independent copy and the old datadir stays an intact +# bootable cluster -- the inode check below asserts exactly that. The old +# datadir's path is given by the pg_upgrade_standby_old_datadir GUC. So a standby is a +# fresh new-version initdb skeleton + a streaming connection + those config +# settings + an empty pg_upgrade.signal sentinel; it never runs pg_upgrade. +# +# The retained old datadir here is the stood-down old standby from section 1a, +# so this only runs same-version, where that standby exists. (The mechanism is +# version-independent; only the same-version test rig for the old datadir is.) +SKIP: +{ + skip 'relink-manifest test reuses the same-version stood-down old standby', 10 + if defined($ENV{oldinstall}) || !defined($oldsby); + + my $olddir = $oldsby->data_dir; + + # The old datadir still has the user relation files from before the upgrade; + # they are the relink source and never travel through the window. + my @userrels = glob("$olddir/base/*/[0-9]*"); + ok(scalar(@userrels) > 0, + 'standby: retained old datadir has the user relation files (relink source)'); + + # A FRESH new-version skeleton -- initdb'd, empty of user data -- is the + # standby target. It points primary_conninfo at the upgraded primary, + # names the retained old datadir in the pg_upgrade_standby_old_datadir GUC, and + # stages an EMPTY pg_upgrade.signal sentinel; startup infers the streaming + # mode from primary_conninfo, streams the window, and its RELINK redo copies + # the user relations in from that old datadir. pg_upgrade_standby_transfer_mode is + # left unset ("mirror"), so the standby reproduces the primary's --copy. + my $standby = PostgreSQL::Test::Cluster->new('standby'); + $standby->init; + $standby->append_conf('postgresql.conf', $primary_conf); + my $sdir = $standby->data_dir; + $standby->append_conf('postgresql.conf', + "primary_conninfo = '" . $new->connstr . "'\n"); + $standby->append_conf('postgresql.conf', + "pg_upgrade_standby_old_datadir = '$olddir'\n"); + open(my $ss, '>', "$sdir/pg_upgrade.signal") or die $!; # empty sentinel + close($ss); + $standby->set_standby_mode; # standby.signal + + # Before streaming, the skeleton has no user table 't' data of its own. + $standby->start; + + # It auto-armed and streamed the window. + like(slurp_file($standby->logfile), + qr/auto-armed streaming standby from locally derived anchor/, + 'standby: auto-armed from the primary over replication'); + + # It converges: the window supplied the system catalogs and the RELINK + # manifest linked the user relations from the old datadir, so the user table + # (and its large objects) match the primary. + $standby->poll_query_until('postgres', 'SELECT count(*) = 2000 FROM t') + or die "standby did not converge to the upgraded data in time"; + $new->wait_for_catchup($standby, 'replay', $new->lsn('insert')); + + is( $standby->safe_psql('postgres', $fp_query), + $want, + 'standby: converged to the upgraded primary data (window + relink manifest)'); + + # The user relation files are now present in the skeleton. + my @linked = glob("$sdir/base/*/[0-9]*"); + ok(scalar(@linked) > 0, + 'standby: user relation files copied into the skeleton by RELINK redo'); + + # They are COPIES, not hardlinks: a skeleton user file and its old-datadir + # source have different inodes, so the old datadir is untouched and stays a + # bootable cluster to roll back to (upstream's copy-transfer guarantee). + { + my $rel = $linked[0]; + $rel =~ s{^\Q$sdir\E/}{}; # e.g. base/5/16384 + my $ino_new = (stat("$sdir/$rel"))[1]; + my $ino_old = (stat("$olddir/$rel"))[1]; + isnt($ino_new, $ino_old, + 'standby: user file is an independent copy, not a hardlink (old datadir intact)'); + } + + # Restart with the pg_upgrade.signal sentinel STILL staged (the operator need + # not remove it): a converged standby must come up as an ordinary hot standby + # and must NOT re-fetch the anchor / re-arm at the old CN. The durable + # pg_control upgrade_finalized flag makes first-startup arming no-op. + ok(-f "$sdir/pg_upgrade.signal", + 'standby: sentinel still present after convergence (not auto-removed)'); + my $sby_bindir = $standby->config_data('--bindir'); + my ($cd_out) = run_command([ "$sby_bindir/pg_controldata", '-D', $sdir ]); + like($cd_out, qr/wal-upgrade window finalized:\s+yes/, + 'standby: durable upgrade_finalized flag set after convergence'); + my $log_before_restart = -s $standby->logfile; + $standby->restart; + is($standby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 't', 'standby: restarts as an ordinary hot standby'); + is($standby->safe_psql('postgres', $fp_query), + $want, 'standby: data still matches the primary after restart'); + # The finalized guard must have short-circuited arming: no new "auto-armed" + # line since the restart (re-arming would rewind recovery behind the standby's + # replay position, and fail once the primary drops the retention slot). + my $log_after = slurp_file($standby->logfile, $log_before_restart); + unlike($log_after, qr/auto-armed streaming standby from locally derived anchor/, + 'standby: converged restart does NOT re-arm at the old CN (finalized guard)'); + + $standby->stop; +} + +# +# 2b. Transfer-mode override: the standby need not mirror the primary's mode. +# +# The primary upgraded with --copy, but this standby sets +# pg_upgrade_standby_transfer_mode = link. The RELINK redo must then HARDLINK each user +# relation from the old datadir instead of copying it, so a skeleton user file +# and its old-datadir source share one inode -- the exact inverse of section 2's +# copy assertion. This proves the mode comes from the standby's own config, not +# the manifest. Reuses the stood-down old standby datadir, so same-version only. +# +SKIP: +{ + skip 'transfer-mode override test reuses the same-version stood-down old standby', 3 + if defined($ENV{oldinstall}) || !defined($oldsby); + + my $olddir = $oldsby->data_dir; + + my $linksby = PostgreSQL::Test::Cluster->new('linksby'); + $linksby->init; + $linksby->append_conf('postgresql.conf', $primary_conf); + my $ldir = $linksby->data_dir; + $linksby->append_conf('postgresql.conf', + "primary_conninfo = '" . $new->connstr . "'\n"); + $linksby->append_conf('postgresql.conf', + "pg_upgrade_standby_old_datadir = '$olddir'\n"); + # Override: place user relations by hardlink even though the primary copied. + $linksby->append_conf('postgresql.conf', + "pg_upgrade_standby_transfer_mode = 'link'\n"); + open(my $ls, '>', "$ldir/pg_upgrade.signal") or die $!; # empty sentinel + close($ls); + $linksby->set_standby_mode; + $linksby->start; + + $linksby->poll_query_until('postgres', 'SELECT count(*) = 2000 FROM t') + or die "link-mode standby did not converge to the upgraded data in time"; + $new->wait_for_catchup($linksby, 'replay', $new->lsn('insert')); + is($linksby->safe_psql('postgres', $fp_query), + $want, 'override: link-mode standby converged to the upgraded data'); + + # At least one relation file in the skeleton must share an inode with its + # counterpart in the old datadir -- i.e. be a hardlink. User relations (the + # ones in the RELINK manifest) are hardlinked under the link override; + # system catalogs, which arrive as fresh RELFILE images in the window, never + # share an inode. Section 2 (copy) has ZERO shared inodes, so a nonzero + # count here proves pg_upgrade_standby_transfer_mode overrode the primary's --copy. + my @relfiles = glob("$ldir/base/*/[0-9]*"); + ok(scalar(@relfiles) > 0, + 'override: relation files present in the link-mode skeleton'); + my $shared = 0; + for my $f (@relfiles) + { + (my $rel = $f) =~ s{^\Q$ldir\E/}{}; + my $ino_new = (stat("$ldir/$rel"))[1]; + my $ino_old = (stat("$olddir/$rel"))[1]; + $shared++ if defined $ino_old && $ino_new == $ino_old; + } + ok($shared > 0, + "override: $shared user relation file(s) hardlinked to the old datadir (mode overridden to link)"); + + $linksby->stop; +} + +# +# 2c. No initdb: a BARE skeleton (empty data dir, never initdb'd) streams the +# window just like an initdb'd one. +# +# The postmaster needs a valid new-version pg_control before it can load it and +# start recovery -- but that file is synthesizable from this binary's own +# constants, so a real initdb is not required. Stage a data directory with only +# the standby-local config (postgresql.conf, pg_hba.conf), an empty +# pg_upgrade.signal sentinel, and standby.signal -- NO PG_VERSION, NO pg_control, +# NO base/. At first startup checkDataDir() synthesizes pg_control + PG_VERSION + +# the subdir skeleton (logs "no initdb was required"), the anchor is derived +# locally, the window streams, and the RELINK redo links user relations from the +# retained old datadir. Same convergence as section 2, without initdb. +# +# Driven with pg_ctl/psql directly (not $node->init, which runs initdb): a bare +# skeleton is exactly what has no initdb. +# +SKIP: +{ + skip 'no-initdb bare-skeleton test reuses the same-version stood-down old standby', 4 + if defined($ENV{oldinstall}) || !defined($oldsby); + + my $olddir = $oldsby->data_dir; + + # A separate PostgreSQL::Test::Cluster gives us its allocated port, host + # (socket dir), data_dir path, and logfile -- but we do NOT call ->init, so + # no initdb runs and the data directory starts out empty. + my $bare = PostgreSQL::Test::Cluster->new('bareskel'); + my $bdir = $bare->data_dir; + mkdir $bdir or die "could not create $bdir: $!"; + chmod 0700, $bdir; + # ->init normally makes these; ->start/->backup teardown expect them to exist. + mkdir $bare->backup_dir; + mkdir $bare->archive_dir; + + # Stage only the standby-local config initdb would otherwise write, plus the + # streaming settings and the empty sentinel. Everything else (pg_control, + # PG_VERSION, the directory skeleton) is synthesized at first startup. + my $sockdir = $bare->host; + open(my $bc, '>', "$bdir/postgresql.conf") or die $!; + print $bc "port = " . $bare->port . "\n"; + print $bc "listen_addresses = '" + . ($PostgreSQL::Test::Utils::windows_os ? '127.0.0.1' : '') . "'\n"; + print $bc "unix_socket_directories = '$sockdir'\n"; + print $bc "hot_standby = on\n"; + print $bc "primary_conninfo = '" . $new->connstr . "'\n"; + print $bc "pg_upgrade_standby_old_datadir = '$olddir'\n"; + close($bc); + open(my $bh, '>', "$bdir/pg_hba.conf") or die $!; + print $bh "local all all trust\n"; + print $bh "host all all 127.0.0.1/32 trust\n"; + print $bh "host all all ::1/128 trust\n"; + close($bh); + open(my $bs, '>', "$bdir/pg_upgrade.signal") or die $!; # empty sentinel + close($bs); + open(my $bst, '>', "$bdir/standby.signal") or die $!; + close($bst); + + # It really is bare -- initdb's outputs are absent before startup. + ok(!-f "$bdir/PG_VERSION" && !-f "$bdir/global/pg_control", + 'no-initdb: skeleton has no PG_VERSION / pg_control before startup (never initdb\'d)'); + + $bare->start; + + # checkDataDir() synthesized the control file rather than finding one. + like(slurp_file($bare->logfile), + qr/synthesized a fresh pg_control and PG_VERSION/, + 'no-initdb: pg_control + PG_VERSION synthesized at startup (no initdb was required)'); + + # Same convergence as the initdb'd standby: window streamed, user relations + # relinked, data matches the primary. + $bare->poll_query_until('postgres', 'SELECT count(*) = 2000 FROM t') + or die "no-initdb standby did not converge to the upgraded data in time"; + $new->wait_for_catchup($bare, 'replay', $new->lsn('insert')); + is($bare->safe_psql('postgres', $fp_query), + $want, 'no-initdb: bare skeleton converged to the upgraded primary data'); + is($bare->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 't', 'no-initdb: bare skeleton is a hot standby (in recovery)'); + + $bare->stop; +} + +# +# 2a. Negative control: the RELINK manifest is load-bearing, and a missing +# old-datadir path is a hard error, not a silent partial recovery. +# +# Stream the SAME schema-only window into a fresh skeleton with the +# pg_upgrade.signal sentinel present (so it arms and streams) but +# pg_upgrade_standby_old_datadir UNSET. The RELINK redo would have no source, so the +# user relations -- which were never in the window -- could only come up ABSENT. +# Rather than silently bring up a hot standby missing all its user data, the redo +# must FATAL at the manifest. This proves both that the relink step is +# load-bearing AND that its misconfiguration fails loudly. Runs only +# same-version, where $new streams. +# +SKIP: +{ + skip 'negative relink test needs the live streaming primary', 2 + if defined($ENV{oldinstall}); + + my $norelink = PostgreSQL::Test::Cluster->new('norelink'); + $norelink->init; + $norelink->append_conf('postgresql.conf', $primary_conf); + my $ndir = $norelink->data_dir; + # primary_conninfo + an EMPTY pg_upgrade.signal, but NO pg_upgrade_standby_old_datadir. + # With LOCAL CN derivation the retained old datadir is needed UP FRONT (to + # derive CN from its control file before streaming), so the skeleton FATALs at + # startup rather than later at relink redo. + $norelink->append_conf('postgresql.conf', + "primary_conninfo = '" . $new->connstr . "'\n"); + $norelink->set_standby_mode; + open(my $ns, '>', "$ndir/pg_upgrade.signal") or die $!; + close($ns); + + # Start fails: the startup process FATALs immediately, so do not use ->start, + # which die()s on a non-running server; launch via pg_ctl and check the + # outcome ourselves. + my $logstart = -s $norelink->logfile; + $norelink->start(fail_ok => 1); + + # Startup FATALs because CN cannot be derived without the retained old + # datadir, refusing to bring the standby up with no anchor / user data. + $norelink->wait_for_log( + qr/streaming --wal-upgrade skeleton requires "pg_upgrade_standby_old_datadir" to derive the upgrade anchor/, + $logstart); + ok(1, + 'negative: startup FATALs when the streaming skeleton has no old datadir to derive CN (no silent partial recovery)'); + + # The server must NOT be serving: a standby missing all user data never opens. + my ($rc, $stdout, $stderr) = + $norelink->psql('postgres', 'SELECT 1', timeout => 10); + isnt($rc, 0, + 'negative: standby does not come up when the relink source is missing'); + + # The startup process FATALed, taking the postmaster down after pg_ctl had + # already reported "started", so the framework still thinks this node is + # running. Sync its state to stopped so END-time teardown does not try (and + # fail) to stop an already-dead server. + $norelink->_update_pid(0); +} + +# +# 2b. Aborted upgrade: the paused standby resumes and keeps following the old +# primary. The handoff pause is reversible, so a --wal-upgrade that is +# signaled but then abandoned does not strand the standby -- the operator +# restarts the old primary read-write and resumes replay, and the standby +# streams the primary's new old-format WAL as before. +# +# This uses its own same-version primary/standby pair: the main $old cluster was +# consumed by the real upgrade above, and the handoff record only exists in the +# patched (same-version) binary. +# +SKIP: +{ + skip 'abort/resume test needs the same-version handoff record', 4 + if defined($ENV{oldinstall}); + + my $apri = PostgreSQL::Test::Cluster->new('abort_primary'); + $apri->init(allows_streaming => 1); + $apri->append_conf('postgresql.conf', $primary_conf); + $apri->start; + $apri->safe_psql('postgres', + 'CREATE TABLE t (id int primary key); INSERT INTO t SELECT generate_series(1, 100)'); + + $apri->backup('abort_base'); + my $asby = PostgreSQL::Test::Cluster->new('abort_standby'); + $asby->init_from_backup($apri, 'abort_base', has_streaming => 1); + $asby->append_conf('postgresql.conf', $primary_conf); + $asby->start; + $apri->wait_for_catchup($asby, 'replay', $apri->lsn('insert')); + + my $logstart = -s $asby->logfile; + + # Signal the handoff (writes the trigger and stops the primary) but do NOT + # run pg_upgrade -- this models an upgrade that was started then abandoned. + command_ok( + [ + 'pg_upgrade', '--no-sync', + '--wal-upgrade-signal-handoff', + '--old-datadir' => $apri->data_dir, + '--old-bindir' => $apri->config_data('--bindir'), + '--socketdir' => $apri->host, + '--old-port' => $apri->port, + ], + 'abort: signal-handoff writes the trigger and stops the primary'); + $apri->_update_pid(0); + + # The standby pauses at the boundary (it does not shut down). + $asby->wait_for_log( + qr/reached pg_upgrade handoff on standby; pausing recovery at the upgrade boundary/, + $logstart); + is($asby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 't', 'abort: standby paused and still in recovery'); + + # Abort: restart the old primary read-write (crash recovery treats the + # handoff as a no-op) and generate more old-format WAL. + $apri->start; + is($apri->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 'f', 'abort: old primary restarted read-write'); + $apri->safe_psql('postgres', 'INSERT INTO t SELECT generate_series(101, 200)'); + + # Resume the standby: replay continues past the (no-op) handoff record and + # the standby follows the old primary's new WAL. + $asby->safe_psql('postgres', 'SELECT pg_wal_replay_resume()'); + $apri->wait_for_catchup($asby, 'replay', $apri->lsn('insert')); + is($asby->safe_psql('postgres', 'SELECT count(*) FROM t'), + '200', 'abort: resumed standby caught up to the old primary past the handoff'); + + $asby->stop; + $apri->stop; +} + +# +# 3. Negative: a fresh new-version skeleton with NO primary and NO local window +# starts as an ordinary cluster -- no silent alternative delivery path. +# +my $lonely = PostgreSQL::Test::Cluster->new('lonely'); +$lonely->init(allows_streaming => 1); +$lonely->append_conf('postgresql.conf', $primary_conf); +$lonely->start; +is($lonely->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 'f', 'negative: plain new cluster with no primary is a normal live cluster'); +is($lonely->safe_psql('postgres', "SELECT count(*) FROM pg_tables WHERE tablename = 't'"), + '0', 'negative: no upgrade window was silently applied'); +$lonely->stop; + +$new->stop; + +done_testing(); diff --git a/src/bin/pg_waldump/pgupgradedesc.c b/src/bin/pg_waldump/pgupgradedesc.c new file mode 120000 index 0000000000..c729dc19b0 --- /dev/null +++ b/src/bin/pg_waldump/pgupgradedesc.c @@ -0,0 +1 @@ +../../../src/backend/access/rmgrdesc/pgupgradedesc.c \ No newline at end of file diff --git a/src/bin/pg_waldump/rmgrdesc.c b/src/bin/pg_waldump/rmgrdesc.c index 931ab8b979..326334e136 100644 --- a/src/bin/pg_waldump/rmgrdesc.c +++ b/src/bin/pg_waldump/rmgrdesc.c @@ -28,6 +28,7 @@ #include "commands/tablespace.h" #include "replication/message.h" #include "replication/origin.h" +#include "access/pgupgrade_wal.h" /* pg_upgrade_desc, pg_upgrade_identify */ #include "rmgrdesc.h" #include "storage/standbydefs.h" #include "utils/relmapper.h" diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl index 53b2f016b8..837bdf3d14 100644 --- a/src/bin/pg_waldump/t/001_basic.pl +++ b/src/bin/pg_waldump/t/001_basic.pl @@ -80,7 +80,8 @@ CommitTs ReplicationOrigin Generic LogicalMessage -XLOG2$/, +XLOG2 +PgUpgrade$/, 'rmgr list'); diff --git a/src/common/file_utils.c b/src/common/file_utils.c index 390e60bd01..6c897266fe 100644 --- a/src/common/file_utils.c +++ b/src/common/file_utils.c @@ -23,7 +23,15 @@ #include #include #include +#ifdef HAVE_COPYFILE_H +#include +#endif +#ifdef __linux__ +#include +#include +#endif +#include "common/file_perm.h" #include "common/file_utils.h" #ifdef FRONTEND #include "common/logging.h" @@ -748,3 +756,119 @@ pg_pwrite_zeros(int fd, size_t size, pgoff_t offset) return total_written; } + +/* + * pg_clone_file -- create "dst" as a reflink (copy-on-write) clone of "src". + * + * Uses copyfile(COPYFILE_CLONE_FORCE) where available (macOS), else + * ioctl(FICLONE) (Linux). "dst" must not already exist. Returns + * PG_REFLINK_UNSUPPORTED when the build has neither primitive, + * PG_REFLINK_ERROR (with *save_errno set) on a syscall failure -- in which case + * a partially created "dst" has been unlinked -- and PG_REFLINK_OK on success. + * + * Shared by pg_upgrade's file transfer and the backend --wal-upgrade relink + * replay so the two reproduce identical on-disk results. + */ +PGReflinkResult +pg_clone_file(const char *src, const char *dst, int *save_errno) +{ + *save_errno = 0; + +#if defined(HAVE_COPYFILE) && defined(COPYFILE_CLONE_FORCE) + if (copyfile(src, dst, NULL, COPYFILE_CLONE_FORCE) < 0) + { + *save_errno = errno; + return PG_REFLINK_ERROR; + } + return PG_REFLINK_OK; +#elif defined(__linux__) && defined(FICLONE) + { + int src_fd; + int dst_fd; + + if ((src_fd = open(src, O_RDONLY | PG_BINARY, 0)) < 0) + { + *save_errno = errno; + return PG_REFLINK_ERROR; + } + if ((dst_fd = open(dst, O_RDWR | O_CREAT | O_EXCL | PG_BINARY, + pg_file_create_mode)) < 0) + { + *save_errno = errno; + close(src_fd); + return PG_REFLINK_ERROR; + } + if (ioctl(dst_fd, FICLONE, src_fd) < 0) + { + *save_errno = errno; + close(dst_fd); + close(src_fd); + unlink(dst); + return PG_REFLINK_ERROR; + } + close(dst_fd); + close(src_fd); + return PG_REFLINK_OK; + } +#else + return PG_REFLINK_UNSUPPORTED; +#endif +} + +/* + * pg_copy_file_range_all -- copy the whole of "src" to a new file "dst" using + * copy_file_range(2) (a server-side / reflink-capable copy on supporting + * filesystems). "dst" must not already exist. Return codes match + * pg_clone_file(). + * + * Shared by pg_upgrade's file transfer and the backend --wal-upgrade relink + * replay. + */ +PGReflinkResult +pg_copy_file_range_all(const char *src, const char *dst, int *save_errno) +{ + *save_errno = 0; + +#if defined(HAVE_COPY_FILE_RANGE) + { + int src_fd; + int dst_fd; + + if ((src_fd = open(src, O_RDONLY | PG_BINARY, 0)) < 0) + { + *save_errno = errno; + return PG_REFLINK_ERROR; + } + if ((dst_fd = open(dst, O_RDWR | O_CREAT | O_EXCL | PG_BINARY, + pg_file_create_mode)) < 0) + { + *save_errno = errno; + close(src_fd); + return PG_REFLINK_ERROR; + } + + /* Loop until copy_file_range() reports end-of-file (a 0 return). */ + for (;;) + { + ssize_t nbytes = copy_file_range(src_fd, NULL, dst_fd, NULL, + SSIZE_MAX, 0); + + if (nbytes < 0) + { + *save_errno = errno; + close(dst_fd); + close(src_fd); + unlink(dst); + return PG_REFLINK_ERROR; + } + if (nbytes == 0) + break; + } + close(dst_fd); + close(src_fd); + return PG_REFLINK_OK; + } +#else + return PG_REFLINK_UNSUPPORTED; +#endif +} diff --git a/src/include/access/clog.h b/src/include/access/clog.h index 7894998c76..2d06436ea5 100644 --- a/src/include/access/clog.h +++ b/src/include/access/clog.h @@ -57,4 +57,7 @@ extern void clog_redo(XLogReaderState *record); extern void clog_desc(StringInfo buf, XLogReaderState *record); extern const char *clog_identify(uint8 info); +/* restore captured CLOG segment during --wal-upgrade replay */ +extern void CLOGUpgradeRestoreSegment(int64 segno, const char *data, Size datalen); + #endif /* CLOG_H */ diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h index 6be5299ab6..53f4dfd535 100644 --- a/src/include/access/multixact.h +++ b/src/include/access/multixact.h @@ -152,6 +152,10 @@ extern void multixact_twophase_postabort(FullTransactionId fxid, uint16 info, extern void multixact_redo(XLogReaderState *record); extern void multixact_desc(StringInfo buf, XLogReaderState *record); extern const char *multixact_identify(uint8 info); + +/* restore captured multixact SLRU segments during --wal-upgrade replay */ +extern void MultiXactOffsetUpgradeRestoreSegment(int64 segno, const char *data, Size datalen); +extern void MultiXactMemberUpgradeRestoreSegment(int64 segno, const char *data, Size datalen); extern char *mxid_to_string(MultiXactId multi, int nmembers, MultiXactMember *members); extern char *mxstatus_to_string(MultiXactStatus status); diff --git a/src/include/access/pgupgrade_wal.h b/src/include/access/pgupgrade_wal.h new file mode 100644 index 0000000000..03e3c61634 --- /dev/null +++ b/src/include/access/pgupgrade_wal.h @@ -0,0 +1,98 @@ +/* + * pgupgrade_wal.h + * + * declarations for RM_PG_UPGRADE_ID WAL redo and emit functions. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/access/pgupgrade_wal.h + */ +#ifndef PGUPGRADE_WAL_H +#define PGUPGRADE_WAL_H + +#include "access/xlogreader.h" +#include "catalog/pg_control.h" +#include "lib/stringinfo.h" + +/* WAL upgrade check -- called from StartupProcessMain() before StartupXLOG() */ +extern bool PerformWalUpgradeIfNeeded(void); + +/* + * True iff the pg_upgrade.signal sentinel is present, i.e. this data directory + * is staged for --wal-upgrade recovery. Shared gate for the detection sites in + * PerformWalUpgradeIfNeeded() and checkDataDir(). + */ +extern bool UpgradeSignalStaged(void); + +/* + * Scan a pg_wal directory for the --wal-upgrade markers (START/COMPLETE) + * and the end-of-upgrade checkpoint (CN) that precedes START. Used by the + * local/primary and archive-PITR startup paths (PerformWalUpgradeIfNeeded). + * Returns false if there is no readable WAL at all. + */ +extern bool UpgradeWalScanMarkers(const char *waldir, bool *found_start, + bool *found_complete, CheckPoint *cn, + XLogRecPtr *cn_lsn, uint64 *wal_sysid); + +/* RM_PG_UPGRADE_ID rmgr callbacks (registered in rmgrlist.h) */ +extern void pg_upgrade_redo(XLogReaderState *record); +extern void pg_upgrade_desc(StringInfo buf, XLogReaderState *record); +extern const char *pg_upgrade_identify(uint8 info); + +/* Emit functions (internal to the WAL-window capture, defined in xlog.c) */ +extern XLogRecPtr XLogWritePgUpgrade(bool is_start, uint32 old_major_version, + uint32 new_major_version); +extern XLogRecPtr XLogWritePgUpgradeHandoff(uint32 old_major_version, + uint32 target_major_version); +extern XLogRecPtr XLogWriteUpgradeSlruData(uint8 slru_type); +extern XLogRecPtr XLogWriteUpgradeRawFile(const char *path); +extern XLogRecPtr XLogWriteUpgradeDirSkel(void); + +/* + * Emit the entire --wal-upgrade window in one C routine. Reachable only via + * the binary-upgrade-gated SQL function binary_upgrade_emit_wal_window(); the + * individual steps are not exposed as SQL. + */ +extern void EmitUpgradeWalWindow(uint32 old_major_version, + uint32 new_major_version, + uint8 transfer_mode, bool skip_complete); + +/* + * Batched emission of relation-file images. Many file chunks are packed into + * each XLOG_UPGRADE_RELFILE_DATA record, up to the max WAL payload. + */ +typedef struct UpgradeRelfileBatch +{ + char *buf; /* accumulation buffer (freed by BatchEnd) */ + Size cap; /* capacity == payload cap */ + Size used; /* bytes accumulated for the current record */ + int nentries; /* entries in the current record */ + int nrecords; /* records flushed so far */ + int nfiles; /* files added so far */ + uint8 xfer_mode; /* relink batch only: pg_upgrade transfer mode + * stamped into each entry */ +} UpgradeRelfileBatch; + +extern void XLogUpgradeRelfileBatchBegin(UpgradeRelfileBatch *b); +extern void XLogUpgradeRelfileBatchAddFile(UpgradeRelfileBatch *b, const char *path, + Oid tsoid, Oid dboid, RelFileNumber rfnum, + uint8 forknum, uint32 segno); +extern void XLogUpgradeRelfileBatchEnd(UpgradeRelfileBatch *b); + +/* + * Batched emission of the XLOG_UPGRADE_RELINK manifest: one fixed-size entry + * per USER relation file that pg_upgrade transferred verbatim (and the window + * therefore omits). No file data, just identities; redo links them from the + * standby's old datadir. Reuses UpgradeRelfileBatch for the accumulation state. + */ +extern void XLogUpgradeRelinkBatchBegin(UpgradeRelfileBatch *b, uint8 xfer_mode); +extern void XLogUpgradeRelinkBatchAdd(UpgradeRelfileBatch *b, + Oid tsoid, Oid dboid, RelFileNumber rfnum, + uint8 forknum, uint32 segno); +extern void XLogUpgradeRelinkBatchEnd(UpgradeRelfileBatch *b); + +/* force-flush SLRU dirty pages bypassing enableFsync -- for pg_upgrade with fsync=off */ +extern void XLogFlushUpgradeSLRU(void); + +#endif /* PGUPGRADE_WAL_H */ diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h index ae32ef16d6..b3ec186a88 100644 --- a/src/include/access/rmgrlist.h +++ b/src/include/access/rmgrlist.h @@ -48,3 +48,5 @@ PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL) PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode) PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode) +/* dedicated resource manager for all pg_upgrade WAL records */ +PG_RMGR(RM_PG_UPGRADE_ID, "PgUpgrade", pg_upgrade_redo, pg_upgrade_desc, pg_upgrade_identify, NULL, NULL, NULL, NULL) diff --git a/src/include/access/slru.h b/src/include/access/slru.h index b4adb1789c..4d4af5e8d1 100644 --- a/src/include/access/slru.h +++ b/src/include/access/slru.h @@ -227,6 +227,10 @@ extern int SimpleLruReadPage_ReadOnly(SlruDesc *ctl, int64 pageno, const void *opaque_data); extern void SimpleLruWritePage(SlruDesc *ctl, int slotno); extern void SimpleLruWriteAll(SlruDesc *ctl, bool allow_redirtied); + +/* restore a captured SLRU segment image during --wal-upgrade replay */ +extern void SlruUpgradeRestoreSegment(SlruDesc *ctl, int64 segno, + const char *data, Size datalen); #ifdef USE_ASSERT_CHECKING extern void SlruPagePrecedesUnitTests(SlruDesc *ctl, int per_page); #else diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h index 4dd9862420..d2a1bc1b8e 100644 --- a/src/include/access/xlog.h +++ b/src/include/access/xlog.h @@ -98,6 +98,26 @@ typedef enum RecoveryState extern PGDLLIMPORT int wal_level; extern PGDLLIMPORT bool XLogLogicalInfo; +/* + * pg_upgrade --wal-upgrade streaming-standby transfer mode (postgresql.conf + * GUC pg_upgrade_standby_transfer_mode). PG_UPGRADE_XFER_MIRROR reproduces the + * per-file mode the primary recorded in the manifest; the others override it. + * Concrete modes map to UPGRADE_RELINK_MODE_* in pg_control.h. See + * ArmFromLocalDerivationIfConfigured(). + */ +typedef enum +{ + PG_UPGRADE_XFER_MIRROR = -1, /* unset: use the manifest's per-file mode */ + PG_UPGRADE_XFER_CLONE = 0, + PG_UPGRADE_XFER_COPY = 1, + PG_UPGRADE_XFER_COPY_FILE_RANGE = 2, + PG_UPGRADE_XFER_LINK = 3, + PG_UPGRADE_XFER_SWAP = 4, +} PgUpgradeTransferMode; + +extern PGDLLIMPORT int pg_upgrade_standby_transfer_mode; +extern PGDLLIMPORT char *pg_upgrade_standby_old_datadir; + /* Is WAL archiving enabled (always or only while server is running normally)? */ #define XLogArchivingActive() \ (AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF) @@ -273,6 +293,47 @@ extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN); extern void XLogPutNextOid(Oid nextOid); extern XLogRecPtr XLogRestorePoint(const char *rpName); extern XLogRecPtr XLogAssignLSN(void); + +/* pg_upgrade WAL markers */ +extern XLogRecPtr XLogWritePgUpgrade(bool is_start, + uint32 old_major_version, + uint32 new_major_version); + +/* SLRU bulk image for pg_upgrade WAL replay */ +extern XLogRecPtr XLogWriteUpgradeSlruData(uint8 slru_type); + +/* arm pg_control at CN for in-process --wal-upgrade recovery */ +struct CheckPoint; +extern void ArmControlFileForUpgradeRecovery(const struct CheckPoint *cn, + XLogRecPtr cn_lsn, + uint64 wal_sysid, + bool for_streaming); + +/* control-file checkpoint LSN, for the "upgrade already applied?" test */ +extern XLogRecPtr GetControlFileCheckPointLSN(void); + +/* + * informational DB_IN_UPGRADE flips during window replay (set at START, + * cleared back to DB_IN_PRODUCTION at COMPLETE) -- diagnostics only + */ +extern void SetControlFileInUpgrade(void); +extern void ClearControlFileInUpgrade(void); +extern void SynthesizeUpgradeStreamControlFile(bool allow_overwrite); + +/* + * --wal-upgrade: durable "window replayed to COMPLETE" flag in pg_control + * (replaces the former pg_upgrade_complete.done marker file). Set at COMPLETE + * redo; paired with a control checkpoint past CN to decide "finalized". + */ +extern void SetControlFileUpgradeFinalized(void); +extern bool GetControlFileUpgradeFinalized(void); +extern void SetControlFileUpgradeStarted(void); +extern bool GetControlFileUpgradeStarted(void); + +/* emit the streaming-handoff trigger at shutdown if pg_upgrade armed it */ +extern void EmitPgUpgradeHandoffIfArmed(void); + +/* batched relation-file images live in access/pgupgrade_wal.h */ extern void UpdateFullPageWrites(void); extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p); extern XLogRecPtr GetRedoRecPtr(void); @@ -333,6 +394,19 @@ extern SessionBackupState get_backup_status(void); #define RECOVERY_SIGNAL_FILE "recovery.signal" #define STANDBY_SIGNAL_FILE "standby.signal" #define BACKUP_LABEL_FILE "backup_label" +/* armed by pg_upgrade --wal-upgrade-signal-handoff; consumed by ShutdownXLOG */ +#define PG_UPGRADE_HANDOFF_SIGNAL_FILE "pg_upgrade_handoff.pending" +/* + * Empty, presence-only sentinel (like standby.signal) the operator stages into + * a --wal-upgrade recovery target; its presence arms upgrade replay at + * first-startup. Recovery mode comes from stock config (primary_conninfo => + * stream from the live primary; recovery.signal => restore_command). The + * standby-local options (old-datadir path, transfer mode) live in + * postgresql.conf GUCs, not in this file. See + * ArmFromLocalDerivationIfConfigured(). + */ +#define PG_UPGRADE_SIGNAL_FILE "pg_upgrade.signal" + #define BACKUP_LABEL_OLD "backup_label.old" #define TABLESPACE_MAP "tablespace_map" diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h index ba7750dca0..961350e156 100644 --- a/src/include/access/xlogrecovery.h +++ b/src/include/access/xlogrecovery.h @@ -150,6 +150,13 @@ extern PGDLLIMPORT TimeLineID recoveryTargetTLI; /* Have we already reached a consistent database state? */ extern PGDLLIMPORT bool reachedConsistency; +/* + * Set while replaying a pg_upgrade --wal-upgrade window (between + * XLOG_UPGRADE_START and XLOG_UPGRADE_COMPLETE). Suppresses hot standby + * activation so no read-only connection sees a half-upgraded cluster. + */ +extern PGDLLIMPORT bool pgUpgradeReplayInProgress; + /* Are we currently in standby mode? */ extern PGDLLIMPORT bool StandbyMode; diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 73ed6a0fa4..3abfa1ef36 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202607022 +#define CATALOG_VERSION_NO 202607023 #endif diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index 80b3a730e0..e52ecf7359 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -17,12 +17,13 @@ #include "access/transam.h" #include "access/xlogdefs.h" +#include "common/relpath.h" /* for RelFileNumber */ #include "pgtime.h" /* for pg_time_t */ #include "port/pg_crc32c.h" /* Version identifier for this pg_control format */ -#define PG_CONTROL_VERSION 1902 +#define PG_CONTROL_VERSION 1905 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 @@ -83,12 +84,198 @@ typedef struct CheckPoint #define XLOG_FPI 0xB0 #define XLOG_ASSIGN_LSN 0xC0 #define XLOG_OVERWRITE_CONTRECORD 0xD0 + +/* + * pg_upgrade WAL record types in RM_PG_UPGRADE_ID. + * Using a dedicated rmgr gives us a clean 0x10-aligned namespace free from + * the XLR_RMGR_INFO_MASK = 0xF0 constraint that limits RM_XLOG_ID to one + * record type per 0x10 bucket. + */ +#define XLOG_UPGRADE_START 0x00 /* upgrade window start marker */ +#define XLOG_UPGRADE_COMPLETE 0x10 /* upgrade window complete marker */ +#define XLOG_UPGRADE_SLRU_DATA 0x20 /* bulk SLRU segment image */ +#define XLOG_UPGRADE_RELFILE_DATA 0x30 /* bulk relation file segment + * image */ +#define XLOG_UPGRADE_RAWFILE 0x50 /* verbatim non-relation file + * image (pg_filenode.map, + * PG_VERSION) */ +#define XLOG_UPGRADE_DIRTREE 0x40 /* logged after-image of the + * initdb directory tree */ +#define XLOG_UPGRADE_HANDOFF 0x60 /* OLD-format + * streaming-handoff trigger, + * emitted in the OLD + * cluster's own WAL just + * before pg_upgrade shuts it + * down */ +#define XLOG_UPGRADE_RELINK 0x70 /* manifest of user relations + * to link from the standby's + * old datadir (not carried in + * the window) */ #define XLOG_CHECKPOINT_REDO 0xE0 #define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0 /* XLOG info values for XLOG2 rmgr */ #define XLOG2_CHECKSUMS 0x00 +/* + * XLOG_UPGRADE_START / XLOG_UPGRADE_COMPLETE -- mark the upgrade window so + * crash recovery and tooling (pg_waldump) can identify it and verify + * atomicity. pg_version[] carries $PGDATA/PG_VERSION, which initdb writes + * outside the server and is not otherwise WAL-logged. See pg_upgrade_redo(). + */ +typedef struct xl_pg_upgrade +{ + uint32 old_major_version; /* old cluster PG_VERSION_NUM major */ + uint32 new_major_version; /* new cluster PG_VERSION_NUM major */ + pg_time_t upgrade_time; /* wall-clock time of this record */ + char pg_version[8]; /* new cluster PG_MAJORVERSION, e.g. "18\n" */ +} xl_pg_upgrade; + +#define SizeOfXLPgUpgrade sizeof(xl_pg_upgrade) + +/* + * XLOG_UPGRADE_HANDOFF -- streaming-standby handoff trigger. Carries no data; + * it is a control signal. Unlike the other pg_upgrade records (new-format WAL + * readable only by the new binary), this is emitted into the old cluster's own + * WAL in the old format just before shutdown, so a physical standby still + * streaming the old primary can read it. On replay a StandbyMode server stops + * cleanly at this LSN and reports that a handoff is beginning. + * target_major_version is informational (log message only). + * See pg_upgrade_redo(). + */ +typedef struct xl_pg_upgrade_handoff +{ + uint32 old_major_version; /* this (old) cluster's major version */ + uint32 target_major_version; /* major version being upgraded to */ + pg_time_t handoff_time; /* wall-clock time of this record */ +} xl_pg_upgrade_handoff; + +#define SizeOfXLPgUpgradeHandoff sizeof(xl_pg_upgrade_handoff) + +/* + * XLOG_UPGRADE_SLRU_DATA -- raw images of consecutive SLRU segment files. + * Each record batches segments first_seg..last_seg of one SLRU directory + * (slru_type; see UPGRADE_SLRU_* below); payload is this header followed by + * total_bytes of raw file data. A directory's segments are split across as + * many records as needed to stay under the WAL record size limit. + * See pg_upgrade_redo(). + */ +typedef struct xl_upgrade_slru_data +{ + uint8 slru_type; /* which SLRU: 0=pg_xact, 1=mxoff, 2=mxmem */ + int64 first_seg; /* first segment file number in this record */ + int64 last_seg; /* last segment file number in this record */ + uint32 total_bytes; /* total bytes of raw SLRU data that follow */ + /* followed by total_bytes of raw segment file data (consecutive segments) */ +} xl_upgrade_slru_data; + +#define SizeOfXLUpgradeSlruData (offsetof(xl_upgrade_slru_data, total_bytes) + sizeof(uint32)) + +/* slru_type values for xl_upgrade_slru_data */ +#define UPGRADE_SLRU_XACT 0 +#define UPGRADE_SLRU_MXOFF 1 +#define UPGRADE_SLRU_MXMEM 2 + +/* directory paths corresponding to the slru_type values above */ +#define UPGRADE_SLRU_DIRS { "pg_xact", "pg_multixact/offsets", "pg_multixact/members" } + +/* + * XLOG_UPGRADE_RELFILE_DATA -- raw images of relation files. One record + * batches many chunks up to the max WAL payload, as a sequence of entries each + * being an xl_upgrade_relfile_entry header followed by its nbytes of data: + * + * [entry_0][data_0][entry_1][data_1] ... [entry_k][data_k] + * + * A segment larger than the payload cap is split into page-aligned chunks (see + * blockoff), possibly across several records. See pg_upgrade_redo(). + */ +typedef struct xl_upgrade_relfile_entry +{ + Oid tablespace_oid; /* tablespace containing the file */ + Oid database_oid; /* database OID (0 for shared relations) */ + RelFileNumber relfilenumber; /* relation file number */ + uint8 forknum; /* fork: 0=main, 1=FSM, 2=VM, 3=init */ + uint32 segno; /* 1GB segment number (0 = base segment) */ + uint32 blockoff; /* first block within the segment for this + * chunk */ + uint32 nbytes; /* bytes of raw page data that follow */ + /* followed by nbytes bytes of raw file data for this chunk */ +} xl_upgrade_relfile_entry; + +#define SizeOfXLUpgradeRelfileEntry sizeof(xl_upgrade_relfile_entry) + +/* + * XLOG_UPGRADE_RELINK -- manifest of user relation files that pg_upgrade + * transferred verbatim (and the window therefore omits, keeping it + * schema-sized). Each entry names one file by identity; no file data follows. + * One record batches many entries: [entry_0][entry_1] ... [entry_k]. Redo is a + * no-op on the primary and places each file from the standby's retained old + * datadir on a streaming standby. See pg_upgrade_redo(). + */ +typedef struct xl_upgrade_relink_entry +{ + Oid tablespace_oid; /* tablespace containing the file */ + Oid database_oid; /* database OID (0 for shared relations) */ + RelFileNumber relfilenumber; /* relation file number */ + uint8 forknum; /* fork: 0=main, 1=FSM, 2=VM, 3=init */ + uint8 transfer_mode; /* selects the placement primitive; see + * UPGRADE_RELINK_MODE_* below and + * relink_place_file() */ + uint32 segno; /* 1GB segment number (0 = base segment) */ +} xl_upgrade_relink_entry; + +/* + * transfer_mode values, mirroring pg_upgrade's transferMode (see + * src/bin/pg_upgrade/pg_upgrade.h); duplicated here because the record is read + * by the backend, which does not include pg_upgrade headers. + */ +#define UPGRADE_RELINK_MODE_CLONE 0 +#define UPGRADE_RELINK_MODE_COPY 1 +#define UPGRADE_RELINK_MODE_COPY_FILE_RANGE 2 +#define UPGRADE_RELINK_MODE_LINK 3 +#define UPGRADE_RELINK_MODE_SWAP 4 + +#define SizeOfXLUpgradeRelinkEntry sizeof(xl_upgrade_relink_entry) + +/* + * XLOG_UPGRADE_RAWFILE -- verbatim image of a non-relation file not reachable + * through the buffer manager (currently pg_filenode.map and PG_VERSION), so the + * cluster can be rebuilt from an empty data directory. Payload is this header, + * then path_len bytes of PGDATA-relative path (no trailing NUL), then data_len + * bytes of contents. See pg_upgrade_redo(). + */ +typedef struct xl_upgrade_rawfile +{ + uint32 path_len; /* length of the PGDATA-relative path */ + uint32 data_len; /* length of the file contents that follow */ + /* followed by path_len bytes of path, then data_len bytes of contents */ +} xl_upgrade_rawfile; + +#define SizeOfXLUpgradeRawfile (offsetof(xl_upgrade_rawfile, data_len) + sizeof(uint32)) + +/* + * XLOG_UPGRADE_DIRTREE -- logged after-image of the new cluster's directory + * tree once pg_upgrade has finished, so recovery can rebuild the skeleton from + * WAL without a surviving on-disk skeleton and without running initdb. Payload + * is this header followed by: + * 1. ndirs NUL-terminated PGDATA-relative directory paths (dir_bytes long), + * emitted parent-before-child; then + * 2. nsymlinks symlink entries (sym_bytes long), each two NUL-terminated + * strings: PGDATA-relative link path, then target. These capture + * user-tablespace symlinks (pg_tblspc/ -> external location). + * See pg_upgrade_redo(). + */ +typedef struct xl_upgrade_dirtree +{ + uint32 ndirs; /* number of directory paths */ + uint32 dir_bytes; /* total bytes of directory-path data */ + uint32 nsymlinks; /* number of symlink entries */ + uint32 sym_bytes; /* total bytes of symlink-entry data */ + /* followed by dir_bytes of dir paths, then sym_bytes of symlink entries */ +} xl_upgrade_dirtree; + +#define SizeOfXLUpgradeDirtree (offsetof(xl_upgrade_dirtree, sym_bytes) + sizeof(uint32)) + /* * System status indicator. Note this is stored in pg_control; if you change @@ -103,6 +290,15 @@ typedef enum DBState DB_IN_CRASH_RECOVERY, DB_IN_ARCHIVE_RECOVERY, DB_IN_PRODUCTION, + + /* + * A --wal-upgrade cluster replaying its upgrade window (between + * XLOG_UPGRADE_START and _COMPLETE). Informational only, so + * pg_controldata shows "in pg_upgrade" for a half-reconstructed cluster; + * it does not drive the recovery-mode decision. Appended last to keep + * on-disk values stable. + */ + DB_IN_UPGRADE, } DBState; /* @@ -237,6 +433,24 @@ typedef struct ControlFileData */ bool default_char_signedness; + /* + * --wal-upgrade: true once the upgrade window has been replayed to + * XLOG_UPGRADE_COMPLETE. Durable "upgrade finalized" signal that lets + * first-startup tell a fully-upgraded cluster from a crashed partial one. + * See PerformWalUpgradeIfNeeded(). + */ + bool upgrade_finalized; + + /* + * --wal-upgrade: true once the burst server has begun emitting the window + * (set just before XLOG_UPGRADE_START, cleared only at finalize). + * Durable evidence that an upgrade started here, so a crashed partial + * upgrade is detectable even when the START-bearing WAL did not survive. + * started && !finalized => refuse to auto-serve. See + * PerformWalUpgradeIfNeeded(). + */ + bool upgrade_started; + /* * Random nonce, used in authentication requests that need to proceed * based on values that are cluster-unique, like a SASL exchange that diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3cb84359ad..1001d81107 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6823,6 +6823,9 @@ { oid => '2849', descr => 'current wal write location', proname => 'pg_current_wal_lsn', provolatile => 'v', prorettype => 'pg_lsn', proargtypes => '', prosrc => 'pg_current_wal_lsn' }, +# The pg_upgrade --wal-upgrade window is emitted by the single +# binary-upgrade-gated function binary_upgrade_emit_wal_window() (see the +# binary_upgrade_* block), not by per-step SQL functions. { oid => '2852', descr => 'current wal insert location', proname => 'pg_current_wal_insert_lsn', provolatile => 'v', prorettype => 'pg_lsn', proargtypes => '', @@ -11994,6 +11997,11 @@ proisstrict => 'f', provolatile => 'v', proparallel => 'u', prorettype => 'void', proargtypes => '', prosrc => 'binary_upgrade_create_conflict_detection_slot' }, +{ oid => '9700', descr => 'for use by pg_upgrade (--wal-upgrade window)', + proname => 'binary_upgrade_emit_wal_window', proisstrict => 'f', + provolatile => 'v', proparallel => 'u', prorettype => 'void', + proargtypes => 'int4 int4 int4 bool', + prosrc => 'binary_upgrade_emit_wal_window' }, # conversion functions { oid => '4310', descr => 'internal conversion function for KOI8R to WIN1251', diff --git a/src/include/common/file_utils.h b/src/include/common/file_utils.h index d6415424b6..81f89aa9c1 100644 --- a/src/include/common/file_utils.h +++ b/src/include/common/file_utils.h @@ -59,6 +59,23 @@ extern ssize_t pg_pwritev_with_retry(int fd, extern ssize_t pg_pwrite_zeros(int fd, size_t size, pgoff_t offset); +/* + * Result of the reflink helpers below. The caller decides how to report a + * failure (pg_fatal in the frontend, ereport in the backend), so these return a + * code rather than raising an error themselves. + */ +typedef enum PGReflinkResult +{ + PG_REFLINK_OK, /* the clone/copy succeeded */ + PG_REFLINK_UNSUPPORTED, /* not compiled with support on this platform */ + PG_REFLINK_ERROR, /* a syscall failed; see *save_errno */ +} PGReflinkResult; + +extern PGReflinkResult pg_clone_file(const char *src, const char *dst, + int *save_errno); +extern PGReflinkResult pg_copy_file_range_all(const char *src, const char *dst, + int *save_errno); + /* Filename components */ #define PG_TEMP_FILES_DIR "pgsql_tmp" #define PG_TEMP_FILE_PREFIX "pgsql_tmp" diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index 8057d7870a..5a6de7c61f 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -345,6 +345,7 @@ extern PGDLLIMPORT bool optimize_bounded_sort; extern PGDLLIMPORT const struct config_enum_entry archive_mode_options[]; extern PGDLLIMPORT const struct config_enum_entry dynamic_shared_memory_options[]; extern PGDLLIMPORT const struct config_enum_entry io_method_options[]; +extern PGDLLIMPORT const struct config_enum_entry pg_upgrade_standby_transfer_mode_options[]; extern PGDLLIMPORT const struct config_enum_entry recovery_target_action_options[]; extern PGDLLIMPORT const struct config_enum_entry server_message_level_options[]; extern PGDLLIMPORT const struct config_enum_entry wal_level_options[]; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ffb413ab61..b554f6c798 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1970,6 +1970,7 @@ PGP_S2K PGPing PGQueryClass PGRUsage +PGReflinkResult PGSemaphore PGSemaphoreData PGShmemHeader @@ -2369,11 +2370,14 @@ PgStat_TableStatus PgStat_TableXactStatus PgStat_WalCounters PgStat_WalStats +PgUpgradeTransferMode PgXmlErrorContext PgXmlStrictness Pg_abi_values Pg_finfo_record Pg_magic_struct +PhysicalSlotInfo +PhysicalSlotInfoArr PipeProtoChunk PipeProtoHeader PlaceHolderInfo @@ -2705,6 +2709,7 @@ ReturningClause ReturningExpr ReturningOption ReturningOptionKind +RevertableOp RevmapContents RevokeRoleGrantAction RewriteMappingDataEntry @@ -2941,6 +2946,7 @@ SlruErrorCause SlruOpts SlruPageStatus SlruScanCallback +SlruSegInfo SlruSegState SlruShared SlruSharedData @@ -3336,12 +3342,14 @@ UnresolvedTup UnresolvedTupData UpdateContext UpdateStmt +UpgradeRelfileBatch UpgradeTask UpgradeTaskProcessCB UpgradeTaskReport UpgradeTaskSlot UpgradeTaskSlotState UpgradeTaskStep +UpgradeWalReadPrivate UploadManifestCmd UpperRelationKind UserAuth @@ -4521,6 +4529,8 @@ xl_multixact_create xl_multixact_truncate xl_overwrite_contrecord xl_parameter_change +xl_pg_upgrade +xl_pg_upgrade_handoff xl_relmap_update xl_replorigin_drop xl_replorigin_set @@ -4534,6 +4544,11 @@ xl_standby_locks xl_tblspc_create_rec xl_tblspc_drop_rec xl_testcustomrmgrs_message +xl_upgrade_dirtree +xl_upgrade_rawfile +xl_upgrade_relfile_entry +xl_upgrade_relink_entry +xl_upgrade_slru_data xl_xact_abort xl_xact_assignment xl_xact_commit -- 2.50.1 (Apple Git-155)