mirror of
				https://github.com/postgres/postgres.git
				synced 2025-10-31 00:03:57 -04:00 
			
		
		
		
	Support RBM_ZERO_AND_CLEANUP_LOCK in ExtendBufferedRelTo(), add tests
For some reason I had not implemented RBM_ZERO_AND_CLEANUP_LOCK support in ExtendBufferedRelTo(), likely thinking it not being reachable. But it is reachable, e.g. when replaying a WAL record for a page in a relation that subsequently is truncated (likely only reachable when doing crash recovery or PITR, not during ongoing streaming replication). As now all of the RBM_* modes are supported, remove assertions checking mode. As we had no test coverage for this scenario, add a new TAP test. There's plenty more that ought to be tested in this area... Reported-by: Tom Lane <tgl@sss.pgh.pa.us> Reported-by: Alexander Lakhin <exclusion@gmail.com> Reviewed-by: Alexander Lakhin <exclusion@gmail.com> Discussion: https://postgr.es/m/392271.1681238924%40sss.pgh.pa.us Discussion: https://postgr.es/m/0b5eb82b-cb99-e0a4-b932-3dc60e2e3926@gmail.com
This commit is contained in:
		
							parent
							
								
									e4d905f772
								
							
						
					
					
						commit
						43a33ef54e
					
				| @ -888,8 +888,6 @@ ExtendBufferedRelTo(ExtendBufferedWhat eb, | |||||||
| 	Assert((eb.rel != NULL) != (eb.smgr != NULL)); | 	Assert((eb.rel != NULL) != (eb.smgr != NULL)); | ||||||
| 	Assert(eb.smgr == NULL || eb.relpersistence != 0); | 	Assert(eb.smgr == NULL || eb.relpersistence != 0); | ||||||
| 	Assert(extend_to != InvalidBlockNumber && extend_to > 0); | 	Assert(extend_to != InvalidBlockNumber && extend_to > 0); | ||||||
| 	Assert(mode == RBM_NORMAL || mode == RBM_ZERO_ON_ERROR || |  | ||||||
| 		   mode == RBM_ZERO_AND_LOCK); |  | ||||||
| 
 | 
 | ||||||
| 	if (eb.smgr == NULL) | 	if (eb.smgr == NULL) | ||||||
| 	{ | 	{ | ||||||
| @ -933,7 +931,13 @@ ExtendBufferedRelTo(ExtendBufferedWhat eb, | |||||||
| 	 */ | 	 */ | ||||||
| 	current_size = smgrnblocks(eb.smgr, fork); | 	current_size = smgrnblocks(eb.smgr, fork); | ||||||
| 
 | 
 | ||||||
| 	if (mode == RBM_ZERO_AND_LOCK) | 	/*
 | ||||||
|  | 	 * Since no-one else can be looking at the page contents yet, there is no | ||||||
|  | 	 * difference between an exclusive lock and a cleanup-strength lock. Note | ||||||
|  | 	 * that we pass the original mode to ReadBuffer_common() below, when | ||||||
|  | 	 * falling back to reading the buffer to a concurrent relation extension. | ||||||
|  | 	 */ | ||||||
|  | 	if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK) | ||||||
| 		flags |= EB_LOCK_TARGET; | 		flags |= EB_LOCK_TARGET; | ||||||
| 
 | 
 | ||||||
| 	while (current_size < extend_to) | 	while (current_size < extend_to) | ||||||
| @ -1008,11 +1012,12 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, | |||||||
| 	{ | 	{ | ||||||
| 		uint32		flags = EB_SKIP_EXTENSION_LOCK; | 		uint32		flags = EB_SKIP_EXTENSION_LOCK; | ||||||
| 
 | 
 | ||||||
| 		Assert(mode == RBM_NORMAL || | 		/*
 | ||||||
| 			   mode == RBM_ZERO_AND_LOCK || | 		 * Since no-one else can be looking at the page contents yet, there is | ||||||
| 			   mode == RBM_ZERO_ON_ERROR); | 		 * no difference between an exclusive lock and a cleanup-strength | ||||||
| 
 | 		 * lock. | ||||||
| 		if (mode == RBM_ZERO_AND_LOCK) | 		 */ | ||||||
|  | 		if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK) | ||||||
| 			flags |= EB_LOCK_FIRST; | 			flags |= EB_LOCK_FIRST; | ||||||
| 
 | 
 | ||||||
| 		return ExtendBufferedRel(EB_SMGR(smgr, relpersistence), | 		return ExtendBufferedRel(EB_SMGR(smgr, relpersistence), | ||||||
| @ -1145,9 +1150,10 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/*
 | 	/*
 | ||||||
| 	 * In RBM_ZERO_AND_LOCK mode, grab the buffer content lock before marking | 	 * In RBM_ZERO_AND_LOCK / RBM_ZERO_AND_CLEANUP_LOCK mode, grab the buffer | ||||||
| 	 * the page as valid, to make sure that no other backend sees the zeroed | 	 * content lock before marking the page as valid, to make sure that no | ||||||
| 	 * page before the caller has had a chance to initialize it. | 	 * other backend sees the zeroed page before the caller has had a chance | ||||||
|  | 	 * to initialize it. | ||||||
| 	 * | 	 * | ||||||
| 	 * Since no-one else can be looking at the page contents yet, there is no | 	 * Since no-one else can be looking at the page contents yet, there is no | ||||||
| 	 * difference between an exclusive lock and a cleanup-strength lock. (Note | 	 * difference between an exclusive lock and a cleanup-strength lock. (Note | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ tests += { | |||||||
|       't/033_replay_tsp_drops.pl', |       't/033_replay_tsp_drops.pl', | ||||||
|       't/034_create_database.pl', |       't/034_create_database.pl', | ||||||
|       't/035_standby_logical_decoding.pl', |       't/035_standby_logical_decoding.pl', | ||||||
|  |       't/036_truncated_dropped.pl', | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										136
									
								
								src/test/recovery/t/036_truncated_dropped.pl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/test/recovery/t/036_truncated_dropped.pl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | |||||||
|  | # Copyright (c) 2021-2023, PostgreSQL Global Development Group | ||||||
|  | 
 | ||||||
|  | # Tests recovery scenarios where the files are shorter than in the common | ||||||
|  | # cases, e.g. due to replaying WAL records of a relation that was subsequently | ||||||
|  | # truncated or dropped. | ||||||
|  | 
 | ||||||
|  | use strict; | ||||||
|  | use warnings; | ||||||
|  | use PostgreSQL::Test::Cluster; | ||||||
|  | use Test::More; | ||||||
|  | 
 | ||||||
|  | my $node = PostgreSQL::Test::Cluster->new('n1'); | ||||||
|  | 
 | ||||||
|  | $node->init(); | ||||||
|  | 
 | ||||||
|  | # Disable autovacuum to guarantee VACUUM can remove rows / truncate relations | ||||||
|  | $node->append_conf( | ||||||
|  | 	'postgresql.conf', qq[ | ||||||
|  | wal_level = 'replica' | ||||||
|  | autovacuum = off | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | $node->start(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Test: Replay replay of PRUNE records for a pre-existing, then dropped, | ||||||
|  | # relation | ||||||
|  | 
 | ||||||
|  | $node->safe_psql( | ||||||
|  | 	'postgres', qq[ | ||||||
|  | CREATE TABLE truncme(i int) WITH (fillfactor = 50); | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 1000); | ||||||
|  | UPDATE truncme SET i = 1; | ||||||
|  | CHECKPOINT; -- ensure relation exists at start of recovery | ||||||
|  | VACUUM truncme; -- generate prune records | ||||||
|  | DROP TABLE truncme; | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | $node->stop('immediate'); | ||||||
|  | 
 | ||||||
|  | ok($node->start(), | ||||||
|  | 	'replay of PRUNE records for a pre-existing, then dropped, relation'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Test: Replay of PRUNE records for a newly created, then dropped, relation | ||||||
|  | 
 | ||||||
|  | $node->safe_psql( | ||||||
|  | 	'postgres', qq[ | ||||||
|  | CREATE TABLE truncme(i int) WITH (fillfactor = 50); | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 1000); | ||||||
|  | UPDATE truncme SET i = 1; | ||||||
|  | VACUUM truncme; -- generate prune records | ||||||
|  | DROP TABLE truncme; | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | $node->stop('immediate'); | ||||||
|  | 
 | ||||||
|  | ok($node->start(), | ||||||
|  | 	'replay of PRUNE records for a newly created, then dropped, relation'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Test: Replay of PRUNE records affecting truncated block. With FPIs used for | ||||||
|  | # PRUNE. | ||||||
|  | 
 | ||||||
|  | $node->safe_psql( | ||||||
|  | 	'postgres', qq[ | ||||||
|  | CREATE TABLE truncme(i int) WITH (fillfactor = 50); | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 1000); | ||||||
|  | UPDATE truncme SET i = 1; | ||||||
|  | CHECKPOINT; -- generate FPIs | ||||||
|  | VACUUM truncme; -- generate prune records | ||||||
|  | TRUNCATE truncme; -- make blocks non-existing | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 10); | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | $node->stop('immediate'); | ||||||
|  | 
 | ||||||
|  | ok($node->start(), | ||||||
|  | 	'replay of PRUNE records affecting truncated block (FPIs)'); | ||||||
|  | 
 | ||||||
|  | is($node->safe_psql('postgres', 'select count(*), sum(i) FROM truncme'), | ||||||
|  | 	'10|55', 'table contents as expected after recovery'); | ||||||
|  | $node->safe_psql('postgres', 'DROP TABLE truncme'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Test replay of PRUNE records for blocks that are later truncated. Without | ||||||
|  | # FPIs used for PRUNE. | ||||||
|  | 
 | ||||||
|  | $node->safe_psql( | ||||||
|  | 	'postgres', qq[ | ||||||
|  | CREATE TABLE truncme(i int) WITH (fillfactor = 50); | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 1000); | ||||||
|  | UPDATE truncme SET i = 1; | ||||||
|  | VACUUM truncme; -- generate prune records | ||||||
|  | TRUNCATE truncme; -- make blocks non-existing | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 10); | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | $node->stop('immediate'); | ||||||
|  | 
 | ||||||
|  | ok($node->start(), | ||||||
|  | 	'replay of PRUNE records affecting truncated block (no FPIs)'); | ||||||
|  | 
 | ||||||
|  | is($node->safe_psql('postgres', 'select count(*), sum(i) FROM truncme'), | ||||||
|  | 	'10|55', 'table contents as expected after recovery'); | ||||||
|  | $node->safe_psql('postgres', 'DROP TABLE truncme'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Test: Replay of partial truncation via VACUUM | ||||||
|  | 
 | ||||||
|  | $node->safe_psql( | ||||||
|  | 	'postgres', qq[ | ||||||
|  | CREATE TABLE truncme(i int) WITH (fillfactor = 50); | ||||||
|  | INSERT INTO truncme SELECT generate_series(1, 1000); | ||||||
|  | UPDATE truncme SET i = i + 1; | ||||||
|  | -- ensure a mix of pre/post truncation rows | ||||||
|  | DELETE FROM truncme WHERE i > 500; | ||||||
|  | 
 | ||||||
|  | VACUUM truncme; -- should truncate relation | ||||||
|  | 
 | ||||||
|  | -- rows at TIDs that previously existed | ||||||
|  | INSERT INTO truncme SELECT generate_series(1000, 1010); | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | $node->stop('immediate'); | ||||||
|  | 
 | ||||||
|  | ok($node->start(), 'replay of partial truncation via VACUUM'); | ||||||
|  | 
 | ||||||
|  | is( $node->safe_psql( | ||||||
|  | 		'postgres', 'select count(*), sum(i), min(i), max(i) FROM truncme'), | ||||||
|  | 	'510|136304|2|1010', | ||||||
|  | 	'table contents as expected after recovery'); | ||||||
|  | $node->safe_psql('postgres', 'DROP TABLE truncme'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | done_testing(); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user