This is an automated email from the ASF dual-hosted git repository.

tuhaihe pushed a commit to branch REL_2_STABLE
in repository https://gitbox.apache.org/repos/asf/cloudberry.git

commit 620a408766751842229e8fd5ac70e70ae65ba3bb
Author: Noah Misch <[email protected]>
AuthorDate: Mon Oct 30 14:46:05 2023 -0700

    amcheck: Distinguish interrupted page deletion from corruption.
    
    This prevents false-positive reports about "the first child of leftmost
    target page is not leftmost of its level", "block %u is not leftmost"
    and "left link/right link pair".  They appeared if amcheck ran before
    VACUUM cleaned things, after a cluster exited recovery between the
    first-stage and second-stage WAL records of a deletion.  Back-patch to
    v11 (all supported versions).
    
    Reviewed by Peter Geoghegan.
    
    Discussion: https://postgr.es/m/[email protected]
    (cherry picked from commit 6eb1b293b3c8c9a544272d3d7ff72dc42ed94879)
---
 contrib/amcheck/t/005_pitr.pl   | 89 +++++++++++++++++++++++++++++++++++++++++
 contrib/amcheck/verify_nbtree.c | 83 ++++++++++++++++++++++++++++++++++++--
 2 files changed, 168 insertions(+), 4 deletions(-)

diff --git a/contrib/amcheck/t/005_pitr.pl b/contrib/amcheck/t/005_pitr.pl
new file mode 100644
index 00000000000..07187a799be
--- /dev/null
+++ b/contrib/amcheck/t/005_pitr.pl
@@ -0,0 +1,89 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test integrity of intermediate states by PITR to those states
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# origin node: generate WAL records of interest.
+my $origin = PostgreSQL::Test::Cluster->new('origin');
+$origin->init(has_archiving => 1, allows_streaming => 1);
+$origin->append_conf('postgresql.conf', 'autovacuum = off');
+$origin->start;
+$origin->backup('my_backup');
+# Create a table with each of 6 PK values spanning 1/4 of a block.  Delete the
+# first four, so one index leaf is eligible for deletion.  Make a replication
+# slot just so pg_waldump will always have access to later WAL.
+my $setup = <<EOSQL;
+BEGIN;
+CREATE EXTENSION amcheck;
+CREATE TABLE not_leftmost (c text);
+ALTER TABLE not_leftmost ALTER c SET STORAGE PLAIN;
+INSERT INTO not_leftmost
+  SELECT repeat(n::text, database_block_size / 4)
+  FROM generate_series(1,6) t(n), pg_control_init();
+ALTER TABLE not_leftmost ADD CONSTRAINT not_leftmost_pk PRIMARY KEY (c);
+DELETE FROM not_leftmost WHERE c ~ '^[1-4]';
+SELECT pg_create_physical_replication_slot('for_waldump', true, false);
+COMMIT;
+EOSQL
+$origin->safe_psql('postgres', $setup);
+my $before_vacuum_walfile =
+  $origin->safe_psql('postgres', "SELECT 
pg_walfile_name(pg_current_wal_lsn())");
+# VACUUM to delete the aforementioned leaf page.  Force an XLogFlush() by
+# dropping a permanent table.  That way, the XLogReader infrastructure can
+# always see VACUUM's records, even under synchronous_commit=off.  Finally,
+# find the LSN of that VACUUM's last UNLINK_PAGE record.
+my $vacuum = <<EOSQL;
+SET synchronous_commit = off;
+VACUUM (VERBOSE, INDEX_CLEANUP ON) not_leftmost;
+CREATE TABLE XLogFlush ();
+DROP TABLE XLogFlush;
+SELECT pg_walfile_name(pg_current_wal_flush_lsn());
+EOSQL
+my $after_unlink_walfile = $origin->safe_psql('postgres', $vacuum);
+$origin->stop;
+my $unlink_lsn = do {
+       local %ENV = $origin->_get_env();
+       my $stdout;
+       run_log(['pg_waldump', '-p', $origin->data_dir . '/pg_wal',
+                        $before_vacuum_walfile, $after_unlink_walfile],
+                       '>', \$stdout);
+       $stdout =~ m|^rmgr: Btree .*, lsn: ([/0-9A-F]+), .*, desc: UNLINK_PAGE 
left|m;
+       $1;
+};
+die "did not find UNLINK_PAGE record" unless $unlink_lsn;
+
+# replica node: amcheck at notable points in the WAL stream
+my $replica = PostgreSQL::Test::Cluster->new('replica');
+$replica->init_from_backup($origin, 'my_backup', has_restoring => 1);
+$replica->append_conf('postgresql.conf',
+       "recovery_target_lsn = '$unlink_lsn'");
+$replica->append_conf('postgresql.conf', 'recovery_target_inclusive = off');
+$replica->append_conf('postgresql.conf', 'recovery_target_action = promote');
+$replica->start;
+$replica->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+  or die "Timed out while waiting for PITR promotion";
+# recovery done; run amcheck
+my $debug = "SET client_min_messages = 'debug1'";
+my ($rc, $stderr);
+$rc = $replica->psql(
+       'postgres',
+       "$debug; SELECT bt_index_parent_check('not_leftmost_pk', true)",
+       stderr => \$stderr);
+print STDERR $stderr, "\n";
+is($rc, 0, "bt_index_parent_check passes");
+like(
+       $stderr,
+       qr/interrupted page deletion detected/,
+       "bt_index_parent_check: interrupted page deletion detected");
+$rc = $replica->psql(
+       'postgres',
+       "$debug; SELECT bt_index_check('not_leftmost_pk', true)",
+       stderr => \$stderr);
+print STDERR $stderr, "\n";
+is($rc, 0, "bt_index_check passes");
+
+done_testing();
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 6c3b3c27ccc..a1b581871e6 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -146,6 +146,9 @@ static void bt_check_every_level(Relation rel, Relation 
heaprel,
                                                                 bool 
rootdescend);
 static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
                                                                                
           BtreeLevel level);
+static bool bt_leftmost_ignoring_half_dead(BtreeCheckState *state,
+                                                                               
   BlockNumber start,
+                                                                               
   BTPageOpaque start_opaque);
 static void bt_recheck_sibling_links(BtreeCheckState *state,
                                                                         
BlockNumber btpo_prev_from_target,
                                                                         
BlockNumber leftcurrent);
@@ -775,7 +778,7 @@ bt_check_level_from_leftmost(BtreeCheckState *state, 
BtreeLevel level)
                         */
                        if (state->readonly)
                        {
-                               if (!P_LEFTMOST(opaque))
+                               if (!bt_leftmost_ignoring_half_dead(state, 
current, opaque))
                                        ereport(ERROR,
                                                        
(errcode(ERRCODE_INDEX_CORRUPTED),
                                                         errmsg("block %u is 
not leftmost in index \"%s\"",
@@ -829,8 +832,16 @@ bt_check_level_from_leftmost(BtreeCheckState *state, 
BtreeLevel level)
                         */
                }
 
-               /* Sibling links should be in mutual agreement */
-               if (opaque->btpo_prev != leftcurrent)
+               /*
+                * Sibling links should be in mutual agreement.  There arises
+                * leftcurrent == P_NONE && btpo_prev != P_NONE when the left 
sibling
+                * of the parent's low-key downlink is half-dead.  (A half-dead 
page
+                * has no downlink from its parent.)  Under heavyweight 
locking, the
+                * last bt_leftmost_ignoring_half_dead() validated this 
btpo_prev.
+                * Without heavyweight locking, validation of the P_NONE case 
remains
+                * unimplemented.
+                */
+               if (opaque->btpo_prev != leftcurrent && leftcurrent != P_NONE)
                        bt_recheck_sibling_links(state, opaque->btpo_prev, 
leftcurrent);
 
                /* Check level */
@@ -911,6 +922,66 @@ nextpage:
        return nextleveldown;
 }
 
+/*
+ * Like P_LEFTMOST(start_opaque), but accept an arbitrarily-long chain of
+ * half-dead, sibling-linked pages to the left.  If a half-dead page appears
+ * under state->readonly, the database exited recovery between the first-stage
+ * and second-stage WAL records of a deletion.
+ */
+static bool
+bt_leftmost_ignoring_half_dead(BtreeCheckState *state,
+                                                          BlockNumber start,
+                                                          BTPageOpaque 
start_opaque)
+{
+       BlockNumber reached = start_opaque->btpo_prev,
+                               reached_from = start;
+       bool            all_half_dead = true;
+
+       /*
+        * To handle the !readonly case, we'd need to accept BTP_DELETED pages 
and
+        * potentially observe nbtree/README "Page deletion and backwards 
scans".
+        */
+       Assert(state->readonly);
+
+       while (reached != P_NONE && all_half_dead)
+       {
+               Page            page = palloc_btree_page(state, reached);
+               BTPageOpaque reached_opaque = (BTPageOpaque) 
PageGetSpecialPointer(page);
+
+               CHECK_FOR_INTERRUPTS();
+
+               /*
+                * Try to detect btpo_prev circular links.  
_bt_unlink_halfdead_page()
+                * writes that side-links will continue to point to the 
siblings.
+                * Check btpo_next for that property.
+                */
+               all_half_dead = P_ISHALFDEAD(reached_opaque) &&
+                       reached != start &&
+                       reached != reached_from &&
+                       reached_opaque->btpo_next == reached_from;
+               if (all_half_dead)
+               {
+                       XLogRecPtr      pagelsn = PageGetLSN(page);
+
+                       /* pagelsn should point to an 
XLOG_BTREE_MARK_PAGE_HALFDEAD */
+                       ereport(DEBUG1,
+                                       (errcode(ERRCODE_NO_DATA),
+                                        errmsg_internal("harmless interrupted 
page deletion detected in index \"%s\"",
+                                                                        
RelationGetRelationName(state->rel)),
+                                        errdetail_internal("Block=%u right 
block=%u page lsn=%X/%X.",
+                                                                               
reached, reached_from,
+                                                                               
LSN_FORMAT_ARGS(pagelsn))));
+
+                       reached_from = reached;
+                       reached = reached_opaque->btpo_prev;
+               }
+
+               pfree(page);
+       }
+
+       return all_half_dead;
+}
+
 /*
  * Raise an error when target page's left link does not point back to the
  * previous target page, called leftcurrent here.  The leftcurrent page's
@@ -951,6 +1022,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
                                                 BlockNumber 
btpo_prev_from_target,
                                                 BlockNumber leftcurrent)
 {
+       /* taking BTPageOpaque from metapage would give irrelevant findings */
+       Assert(leftcurrent != P_NONE);
+
        if (!state->readonly)
        {
                Buffer          lbuf;
@@ -1934,7 +2008,8 @@ bt_child_highkey_check(BtreeCheckState *state,
                opaque = (BTPageOpaque) PageGetSpecialPointer(page);
 
                /* The first page we visit at the level should be leftmost */
-               if (first && !BlockNumberIsValid(state->prevrightlink) && 
!P_LEFTMOST(opaque))
+               if (first && !BlockNumberIsValid(state->prevrightlink) &&
+                       !bt_leftmost_ignoring_half_dead(state, blkno, opaque))
                        ereport(ERROR,
                                        (errcode(ERRCODE_INDEX_CORRUPTED),
                                         errmsg("the first child of leftmost 
target page is not leftmost of its level in index \"%s\"",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to