1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Test that VM clear registers VM buffers

The WAL summarizer only tracks registered buffers, so unregistered VM
clears are ommitted from incremental backups, corrupting the restored
visibility map. Test those cases are now fixed.

Author: Melanie Plageman <melanieplageman@gmail.com>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Discussion: https://postgr.es/m/oqcsevg35xjan2327x5kdfth6q4fgeqboxfo3v3imeyih2uiny%406sez5dzxl6nt
Backpatch-through: 17
This commit is contained in:
Melanie Plageman
2026-07-15 17:37:34 -04:00
parent f581fa729d
commit 4d7feebfbd
3 changed files with 261 additions and 0 deletions

View File

@@ -12,6 +12,8 @@
PGFILEDESC = "pg_combinebackup - combine incremental backups"
PGAPPICON=win32
EXTRA_INSTALL=contrib/pg_visibility
subdir = src/bin/pg_combinebackup
top_builddir = ../../..
include $(top_builddir)/src/Makefile.global

View File

@@ -39,6 +39,7 @@ tests += {
't/009_no_full_file.pl',
't/010_hardlink.pl',
't/011_ib_truncation.pl',
't/012_vm_consistency.pl',
],
}
}

View File

@@ -0,0 +1,258 @@
# Copyright (c) 2026, PostgreSQL Global Development Group
#
# Test that heap operations clearing visibility map bits (INSERT, UPDATE,
# DELETE, SELECT FOR UPDATE, COPY) correctly register visibility map buffers,
# since incremental backups rely on the WAL summarizer, which only tracks
# registered buffers.
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
my $tempdir = PostgreSQL::Test::Utils::tempdir_short();
my $mode = $ENV{PG_TEST_PG_COMBINEBACKUP_MODE} || '--copy';
# Set up primary with WAL summarization enabled.
my $primary = PostgreSQL::Test::Cluster->new('primary');
$primary->init(allows_streaming => 1);
$primary->append_conf('postgresql.conf', <<EOF);
summarize_wal = on
autovacuum = off
EOF
$primary->start;
$primary->safe_psql('postgres', q{CREATE EXTENSION pg_visibility});
my @tests = (
{
label => 'INSERT',
table => 'vm_insert_test',
setup => q{CREATE TABLE vm_insert_test (id int);
INSERT INTO vm_insert_test DEFAULT VALUES;},
modify => q{INSERT INTO vm_insert_test VALUES (1)},
visible_op => '<',
frozen_op => '<',
},
{
label => 'DELETE',
table => 'vm_delete_test',
setup => q{CREATE TABLE vm_delete_test (id int);
INSERT INTO vm_delete_test VALUES (1), (2);},
modify => q{DELETE FROM vm_delete_test WHERE id = 1},
visible_op => '<',
frozen_op => '<',
},
{
label => 'UPDATE',
table => 'vm_update_test',
# Include both same-page and cross-page updates. val is stored PLAIN
# so the large update stays inline.
setup => q{CREATE TABLE vm_update_test (id INT, val TEXT);
ALTER TABLE vm_update_test ALTER COLUMN val SET STORAGE PLAIN;
INSERT INTO vm_update_test VALUES (1, 'same page'), (2, 'cross page');
INSERT INTO vm_update_test SELECT i, repeat('a', 200)
FROM generate_series(3, 70) i;},
modify => q{UPDATE vm_update_test SET id = 0 WHERE id = 1;
UPDATE vm_update_test SET val = repeat('b', 4000) WHERE id = 2;},
# Confirm the small update stays on the same heap page while the large
# update relocates the tuple to a different heap page.
ctid_checks => [
{
before => 'id = 1',
after => 'id = 0',
same => 1,
desc => 'small update stays on the same heap page',
},
{
before => 'id = 2',
after => 'id = 2',
same => 0,
desc => 'large update moves the tuple to a different heap page',
},
],
visible_op => '<',
frozen_op => '<',
},
{
label => 'LOCK',
table => 'vm_lock_test',
setup => q{CREATE TABLE vm_lock_test (id int);
INSERT INTO vm_lock_test VALUES (1), (2);},
modify => q{SELECT * FROM vm_lock_test WHERE id = 1 FOR UPDATE},
visible_op => '==',
frozen_op => '<',
},
{
label => 'COPY',
table => 'vm_copy_test',
setup => q{CREATE TABLE vm_copy_test (id int);
INSERT INTO vm_copy_test DEFAULT VALUES;},
modify => q{COPY vm_copy_test FROM PROGRAM 'echo 42'},
visible_op => '<',
frozen_op => '<',
},
);
sub get_vm_summary
{
my ($node, $table) = @_;
my $result = $node->safe_psql('postgres',
"SELECT all_visible, all_frozen FROM pg_visibility_map_summary('$table')");
my @vals = split(/\|/, $result);
return @vals;
}
# Return the heap block number of the (single) row matching $where. The ctid
# is "(block,offset)"; casting it through point lets us pull out the block.
sub heap_block
{
my ($node, $table, $where) = @_;
return $node->safe_psql('postgres',
"SELECT (ctid::text::point)[0]::int FROM $table WHERE $where");
}
# Confirm VACUUM (FREEZE) set VM bits before testing whether later heap
# modifications clear those bits and are captured by incremental backup. We
# could perhaps be more exact than > 0, but the coarseness attempts to avoid
# test flakes.
sub check_vacuumed_vm
{
my ($node, $test) = @_;
my ($all_visible, $all_frozen) = get_vm_summary($node, $test->{table});
cmp_ok($all_visible, '>', 0,
"$test->{label} test: pages are all-visible after vacuum");
cmp_ok($all_frozen, '>', 0,
"$test->{label} test: pages are all-frozen after vacuum");
return ($all_visible, $all_frozen);
}
# Check the VM bit counts after a heap modification against the post-vacuum
# baseline. Most operations clear both bits; tuple locking clears all-frozen
# without clearing all-visible.
sub check_modified_vm
{
my ($node, $test) = @_;
my ($post_visible, $post_frozen) = get_vm_summary($node, $test->{table});
cmp_ok($post_visible, $test->{visible_op}, $test->{pre_visible},
"$test->{label} test: all-visible state after modification");
cmp_ok($post_frozen, $test->{frozen_op}, $test->{pre_frozen},
"$test->{label} test: all-frozen state after modification");
return ($post_visible, $post_frozen);
}
# Verify the combined backup restored VM state exactly as it exists on the
# primary, and ask pg_visibility to check that visible tuples are consistent.
sub validate_restored_vm
{
my ($restored, $test) = @_;
my ($primary_visible, $primary_frozen) =
get_vm_summary($primary, $test->{table});
my ($restored_visible, $restored_frozen) =
get_vm_summary($restored, $test->{table});
is($restored_visible, $primary_visible,
"$test->{label} test: restored all_visible count matches primary");
is($restored_frozen, $primary_frozen,
"$test->{label} test: restored all_frozen count matches primary");
my $corrupt_tids = $restored->safe_psql('postgres',
"SELECT count(*) FROM pg_check_visible('$test->{table}')");
is($corrupt_tids, '0',
"$test->{label} test: no VM corruption detected by pg_check_visible");
}
# Create and populate the tables, then vacuum freeze them to set the VM bits
foreach my $test (@tests)
{
$primary->safe_psql('postgres', $test->{setup});
$primary->safe_psql('postgres', "VACUUM (FREEZE) $test->{table}");
($test->{pre_visible}, $test->{pre_frozen}) =
check_vacuumed_vm($primary, $test);
}
# Take a full backup
my $full_name = 'full';
my $full_path = $primary->backup_dir . "/$full_name";
$primary->command_ok(
[
'pg_basebackup', '--no-sync',
'--pgdata' => $full_path,
'--checkpoint' => 'fast',
],
'full backup');
# Modify the tables and check that the VM bits are as expected for that test
# after the specified modification.
foreach my $test (@tests)
{
# Record the heap block of any rows whose same-/cross-page movement we
# want to verify, before the modification relocates them.
foreach my $check (@{ $test->{ctid_checks} // [] })
{
$check->{before_blk} =
heap_block($primary, $test->{table}, $check->{before});
}
$primary->safe_psql('postgres', $test->{modify});
($test->{post_visible}, $test->{post_frozen}) =
check_modified_vm($primary, $test);
# Confirm each checked row did (or did not) move to a different heap page.
foreach my $check (@{ $test->{ctid_checks} // [] })
{
my $after_blk = heap_block($primary, $test->{table}, $check->{after});
if ($check->{same})
{
is($after_blk, $check->{before_blk},
"$test->{label} test: $check->{desc}");
}
else
{
isnt($after_blk, $check->{before_blk},
"$test->{label} test: $check->{desc}");
}
}
}
# Take an incremental backup. This will have the changes made in the
# modification step.
my $incr_name = 'incr';
my $incr_path = $primary->backup_dir . "/$incr_name";
$primary->command_ok(
[
'pg_basebackup', '--no-sync',
'--pgdata' => $incr_path,
'--checkpoint' => 'fast',
'--incremental' => $full_path . '/backup_manifest',
],
'incremental backup');
# Start a server from a combined backup composed of the incremental and full
# backup.
my $restored = PostgreSQL::Test::Cluster->new('restored');
$restored->init_from_backup($primary, $incr_name,
combine_with_prior => [$full_name],
combine_mode => $mode);
$restored->append_conf('postgresql.conf', <<EOF);
autovacuum = off
EOF
$restored->start;
$restored->safe_psql('postgres', q{CREATE EXTENSION IF NOT EXISTS pg_visibility});
# Confirm that the restored server's visibility map matches the original server
foreach my $test (@tests)
{
validate_restored_vm($restored, $test);
}
$restored->stop;
$primary->stop;
done_testing();