Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1qoETk-000ZjK-72 for pgsql-hackers@arkaria.postgresql.org; Thu, 05 Oct 2023 02:52:44 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1qoETh-0089YS-4G for pgsql-hackers@arkaria.postgresql.org; Thu, 05 Oct 2023 02:52:42 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1qoETg-0089YC-IU for pgsql-hackers@lists.postgresql.org; Thu, 05 Oct 2023 02:52:41 +0000 Received: from mail-yb1-xb35.google.com ([2607:f8b0:4864:20::b35]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.94.2) (envelope-from ) id 1qoETe-00068L-Fv for pgsql-hackers@postgresql.org; Thu, 05 Oct 2023 02:52:40 +0000 Received: by mail-yb1-xb35.google.com with SMTP id 3f1490d57ef6-d862533ea85so633235276.0 for ; Wed, 04 Oct 2023 19:52:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=leadboat.com; s=google; t=1696474356; x=1697079156; darn=postgresql.org; h=user-agent:content-disposition:mime-version:message-id:subject:to :from:date:from:to:cc:subject:date:message-id:reply-to; bh=dMDJedfePgGU9LyTb8Kd9zKeGILOjp9VouD9K2rJh9E=; b=cGMGZwKaga5w/OS01OkguTJaYm86vjzoRdva9Jn47ICs+n13883Hx99lHcBLP75vmJ WewSTw/waAac6tS7w595VKJjtqarygSkbTqc8t50sZVk8Vx5niHTollYFvyGrQXKgY/l vaDvx8janGZZcloDZf657F4lSq2+lf9wXsYAk= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1696474356; x=1697079156; h=user-agent:content-disposition:mime-version:message-id:subject:to :from:date:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=dMDJedfePgGU9LyTb8Kd9zKeGILOjp9VouD9K2rJh9E=; b=mnnHkSz6IeQqEdKsTG10/iHGItg1n2HcA6HG3YNS3icijDKRoZ8dRW10WV/BWDIZDV 1hRUXbptB486gjFRdBUW/SWlakT+iSv4TtA2t59kbwsmzE16RGDEM1lMsK+wMKepQWsG MycsbaSxCFMiFiRX0bhC+1NGMlrgxBrW+AI3KObNDXE9ROQlI+NmH2jPCt21qibeeO8g VKH0GURxIR3QdAoykwM1W8TVoDcgC2dVuGyANzXg2Rs+LNlNl1GLCOFfYHA+aSU0VneR 1nAVWj/xUkFWBSFYBYaAAjk/klJcw1r3Cvo7YawGvKHhHuZIh7B0GarT3Xowy1ooW/P3 RAkw== X-Gm-Message-State: AOJu0Yyd31qtYQnOVo6WzadrrUks4M33+vbFSOduCVV4bVczk8uo7R0N DOQovgXnPOpK38EZBpB2TFuar7LsqF4UUlINqIg= X-Google-Smtp-Source: AGHT+IFnDCPgp0396rwH5NZWPD6L6rpri3D9epVp0dxWh66uDnsEUyu8bfnTIh+q4YOwLwYhLylvXw== X-Received: by 2002:a05:6902:1542:b0:d81:58d3:cc72 with SMTP id r2-20020a056902154200b00d8158d3cc72mr4884260ybu.36.1696474356098; Wed, 04 Oct 2023 19:52:36 -0700 (PDT) Received: from google.com ([2600:1702:a20:5750::2e]) by smtp.gmail.com with ESMTPSA id h9-20020a25d009000000b00d7ba7de90casm128256ybg.51.2023.10.04.19.52.34 for (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 04 Oct 2023 19:52:35 -0700 (PDT) Date: Wed, 4 Oct 2023 19:52:32 -0700 From: Noah Misch To: pgsql-hackers@postgresql.org Subject: post-recovery amcheck expectations Message-ID: <20231005025232.c7.nmisch@google.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="5vsjalvv5R6X7A+b" Content-Disposition: inline User-Agent: Mutt/2.2.12 (2023-09-09) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --5vsjalvv5R6X7A+b Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Suppose we start with this nbtree (subset of a diagram from verify_nbtree.c): * 1 * / \ * 2 <-> 3 We're deleting 2, the leftmost leaf under a leftmost internal page. After the MARK_PAGE_HALFDEAD record, the first downlink from 1 will lead to 3, which still has a btpo_prev pointing to 2. bt_index_parent_check() complains here: /* The first page we visit at the level should be leftmost */ if (first && !BlockNumberIsValid(state->prevrightlink) && !P_LEFTMOST(opaque)) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("the first child of leftmost target page is not leftmost of its level in index \"%s\"", RelationGetRelationName(state->rel)), errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.", state->targetblock, blkno, LSN_FORMAT_ARGS(state->targetlsn)))); One can encounter this if recovery ends between a MARK_PAGE_HALFDEAD record and its corresponding UNLINK_PAGE record. See the attached test case. The index is actually fine in such a state, right? I lean toward fixing this by having amcheck scan left; if left links reach only half-dead or deleted pages, that's as good as the present child block being P_LEFTMOST. There's a different error from bt_index_check(), and I've not yet studied how to fix that: ERROR: left link/right link pair in index "not_leftmost_pk" not in agreement DETAIL: Block=0 left block=0 left link from block=4294967295. Alternatively, one could view this as a need for the user to VACUUM between recovery and amcheck. The documentation could direct users to "VACUUM (DISABLE_PAGE_SKIPPING off, INDEX_CLEANUP on, TRUNCATE off)" if not done since last recovery. Does anyone prefer that or some other alternative? For some other amcheck expectations, the comments suggest reliance on the bt_index_parent_check() ShareLock. I haven't tried to make test cases for them, but perhaps recovery can trick them the same way. Examples: errmsg("downlink or sibling link points to deleted block in index \"%s\"", errmsg("block %u is not leftmost in index \"%s\"", errmsg("block %u is not true root in index \"%s\"", Thanks, nm --5vsjalvv5R6X7A+b Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="amcheck-post-recovery-v0.patch" Author: Noah Misch Commit: Noah Misch diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile index b82f221..9a7f4f7 100644 --- a/contrib/amcheck/Makefile +++ b/contrib/amcheck/Makefile @@ -13,6 +13,7 @@ PGFILEDESC = "amcheck - function for verifying relation integrity" REGRESS = check check_btree check_heap TAP_TESTS = 1 +EXTRA_INSTALL=contrib/pg_walinspect ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/amcheck/t/004_pitr.pl b/contrib/amcheck/t/004_pitr.pl new file mode 100644 index 0000000..ec6d87e --- /dev/null +++ b/contrib/amcheck/t/004_pitr.pl @@ -0,0 +1,65 @@ + +# 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_walinspect will always have access to later WAL. +my $setup = <safe_psql('postgres', $setup); +my $before_vacuum_lsn = + $origin->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); +# VACUUM to delete the aforementioned leaf page. Find the LSN of that +# UNLINK_PAGE record. +my $exec = <safe_psql('postgres', $exec); +$origin->stop; + +# 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 +is( $replica->psql( + 'postgres', "SELECT bt_index_parent_check('not_leftmost_pk', true)"), + 0); +is( $replica->psql( + 'postgres', "SELECT bt_index_check('not_leftmost_pk', true)"), + 0); + +done_testing(); --5vsjalvv5R6X7A+b--