]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix unlogged sequence corruption after standby promotion
authorFujii Masao <fujii@postgresql.org>
Mon, 29 Jun 2026 23:48:47 +0000 (08:48 +0900)
committerFujii Masao <fujii@postgresql.org>
Mon, 29 Jun 2026 23:50:08 +0000 (08:50 +0900)
Previously, if an unlogged sequence was created on the primary and
replicated to a standby, reading the sequence after promoting the
standby (for example, with nextval()) could trigger the following
assertion failure:

    TRAP: failed Assert("((const PageHeaderData *) page)->pd_special >= SizeOfPageHeaderData")

In non-assert builds, the same operation could instead fail with an
error such as:

    ERROR:  bad magic number in sequence

The problem was that seq_redo() updated the init fork page in shared
buffers but did not flush it to disk. During promotion,
ResetUnloggedRelations() recreates the main fork of unlogged
relations by copying the init fork from disk, bypassing shared
buffers. As a result, the main fork could be recreated from a stale
init fork instead of the WAL-replayed page.

Fix this by introducing a helper to flush init fork buffers
immediately, and make seq_redo() use it. As a result, the main fork
of an unlogged sequence is recreated from the up-to-date init fork on
disk, allowing the unlogged sequence to be read successfully after
standby promotion.

Backpatch to v15, where unlogged sequences were introduced.

Author: Fujii Masao <masao.fujii@gmail.com>
Reviewed-by: vignesh C <vignesh21@gmail.com>
Discussion: https://postgr.es/m/CAHGQGwH1Ssze3XM6wjoTjSLVOR041c6xP+vsdLP951=w8oG8bA@mail.gmail.com
Backpatch-through: 15

src/backend/access/hash/hash_xlog.c
src/backend/access/transam/xlogutils.c
src/backend/commands/sequence_xlog.c
src/include/access/xlogutils.h
src/test/recovery/meson.build
src/test/recovery/t/054_unlogged_sequence_promotion.pl [new file with mode: 0644]

index 2060620c7dec923c9a6e0b1fbf290224b9426527..e9a2b9aa9a7236b8ece4f583dbb3a5fd749f185b 100644 (file)
@@ -29,7 +29,6 @@ hash_xlog_init_meta_page(XLogReaderState *record)
        XLogRecPtr      lsn = record->EndRecPtr;
        Page            page;
        Buffer          metabuf;
-       ForkNumber      forknum;
 
        xl_hash_init_meta_page *xlrec = (xl_hash_init_meta_page *) XLogRecGetData(record);
 
@@ -41,16 +40,7 @@ hash_xlog_init_meta_page(XLogReaderState *record)
        page = BufferGetPage(metabuf);
        PageSetLSN(page, lsn);
        MarkBufferDirty(metabuf);
-
-       /*
-        * Force the on-disk state of init forks to always be in sync with the
-        * state in shared buffers.  See XLogReadBufferForRedoExtended.  We need
-        * special handling for init forks as create index operations don't log a
-        * full page image of the metapage.
-        */
-       XLogRecGetBlockTag(record, 0, NULL, &forknum, NULL);
-       if (forknum == INIT_FORKNUM)
-               FlushOneBuffer(metabuf);
+       XLogFlushBufferForRedoIfInit(record, 0, metabuf);
 
        /* all done */
        UnlockReleaseBuffer(metabuf);
@@ -68,7 +58,6 @@ hash_xlog_init_bitmap_page(XLogReaderState *record)
        Page            page;
        HashMetaPage metap;
        uint32          num_buckets;
-       ForkNumber      forknum;
 
        xl_hash_init_bitmap_page *xlrec = (xl_hash_init_bitmap_page *) XLogRecGetData(record);
 
@@ -79,16 +68,7 @@ hash_xlog_init_bitmap_page(XLogReaderState *record)
        _hash_initbitmapbuffer(bitmapbuf, xlrec->bmsize, true);
        PageSetLSN(BufferGetPage(bitmapbuf), lsn);
        MarkBufferDirty(bitmapbuf);
-
-       /*
-        * Force the on-disk state of init forks to always be in sync with the
-        * state in shared buffers.  See XLogReadBufferForRedoExtended.  We need
-        * special handling for init forks as create index operations don't log a
-        * full page image of the metapage.
-        */
-       XLogRecGetBlockTag(record, 0, NULL, &forknum, NULL);
-       if (forknum == INIT_FORKNUM)
-               FlushOneBuffer(bitmapbuf);
+       XLogFlushBufferForRedoIfInit(record, 0, bitmapbuf);
        UnlockReleaseBuffer(bitmapbuf);
 
        /* add the new bitmap page to the metapage's list of bitmaps */
@@ -109,10 +89,7 @@ hash_xlog_init_bitmap_page(XLogReaderState *record)
 
                PageSetLSN(page, lsn);
                MarkBufferDirty(metabuf);
-
-               XLogRecGetBlockTag(record, 1, NULL, &forknum, NULL);
-               if (forknum == INIT_FORKNUM)
-                       FlushOneBuffer(metabuf);
+               XLogFlushBufferForRedoIfInit(record, 1, metabuf);
        }
        if (BufferIsValid(metabuf))
                UnlockReleaseBuffer(metabuf);
index fdc341d8fa48bc20aaca28ddf66581cae57b6400..d8c179c5dccb25a0f9b600b5090bbd8d1513a0f6 100644 (file)
@@ -321,6 +321,28 @@ XLogInitBufferForRedo(XLogReaderState *record, uint8 block_id)
        return buf;
 }
 
+/*
+ * If a redo routine modified an init fork, flush the buffer immediately.
+ *
+ * At the end of crash recovery the init forks of unlogged relations are
+ * copied to the main fork directly from disk, without going through shared
+ * buffers. Therefore, redo routines that update init forks without
+ * restoring a full-page image must call this after setting the page LSN and
+ * marking the buffer dirty.
+ */
+void
+XLogFlushBufferForRedoIfInit(XLogReaderState *record, uint8 block_id,
+                                                        Buffer buffer)
+{
+       ForkNumber      forknum;
+
+       Assert(BufferIsValid(buffer));
+
+       XLogRecGetBlockTag(record, block_id, NULL, &forknum, NULL);
+       if (forknum == INIT_FORKNUM)
+               FlushOneBuffer(buffer);
+}
+
 /*
  * XLogReadBufferForRedoExtended
  *             Like XLogReadBufferForRedo, but with extra options.
@@ -398,7 +420,9 @@ XLogReadBufferForRedoExtended(XLogReaderState *record,
                 * At the end of crash recovery the init forks of unlogged relations
                 * are copied, without going through shared buffers. So we need to
                 * force the on-disk state of init forks to always be in sync with the
-                * state in shared buffers.
+                * state in shared buffers. Use XLogFlushBufferForRedoIfInit() for
+                * redo routines that dirty init-fork buffers without restoring a
+                * full-page image.
                 */
                if (forknum == INIT_FORKNUM)
                        FlushOneBuffer(*buf);
index d0aed48e268011488ee893dbe92016920d3edce9..fcb3230cf3be0fceef753e500892ed8cf740188d 100644 (file)
@@ -63,6 +63,7 @@ seq_redo(XLogReaderState *record)
 
        memcpy(page, localpage, BufferGetPageSize(buffer));
        MarkBufferDirty(buffer);
+       XLogFlushBufferForRedoIfInit(record, 0, buffer);
        UnlockReleaseBuffer(buffer);
 
        pfree(localpage);
index b97387c6d4c4332c430ee7001d782e2d87d8ce83..0c6c74100694f0ea92388f6f6b763f1381e14aca 100644 (file)
@@ -87,6 +87,8 @@ typedef struct ReadLocalXLogPageNoWaitPrivate
 extern XLogRedoAction XLogReadBufferForRedo(XLogReaderState *record,
                                                                                        uint8 block_id, Buffer *buf);
 extern Buffer XLogInitBufferForRedo(XLogReaderState *record, uint8 block_id);
+extern void XLogFlushBufferForRedoIfInit(XLogReaderState *record,
+                                                                                uint8 block_id, Buffer buffer);
 extern XLogRedoAction XLogReadBufferForRedoExtended(XLogReaderState *record,
                                                                                                        uint8 block_id,
                                                                                                        ReadBufferMode mode, bool get_cleanup_lock,
index 9eb8ed114254a66a508e8bfaef7616fd7b376d3d..ad0d85f41897e7a111186c531fa0dcb0efa225ac 100644 (file)
@@ -62,6 +62,7 @@ tests += {
       't/051_effective_wal_level.pl',
       't/052_checkpoint_segment_missing.pl',
       't/053_standby_login_event_trigger.pl',
+      't/054_unlogged_sequence_promotion.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/054_unlogged_sequence_promotion.pl b/src/test/recovery/t/054_unlogged_sequence_promotion.pl
new file mode 100644 (file)
index 0000000..96d1e4b
--- /dev/null
@@ -0,0 +1,34 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that unlogged sequences created on a primary can be read after
+# promotion of a standby that replayed their init fork.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 1);
+$node_primary->start;
+
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my $node_standby = PostgreSQL::Test::Cluster->new('standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+       has_streaming => 1);
+$node_standby->start;
+
+# Create the unlogged sequence after the standby has started, so its init fork
+# is generated by WAL replay on the standby.
+$node_primary->safe_psql('postgres', "CREATE UNLOGGED SEQUENCE ulseq");
+$node_primary->wait_for_replay_catchup($node_standby);
+
+$node_standby->promote;
+
+is($node_standby->safe_psql('postgres', "SELECT nextval('ulseq')"),
+       1, 'unlogged sequence can be read after standby promotion');
+
+done_testing();