-
Notifications
You must be signed in to change notification settings - Fork 201
Fix replay of create database record for missing tablespace. #1585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
reshke
wants to merge
3
commits into
main
Choose a base branch
from
fix_for_db_records
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+286
−51
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,7 @@ | |
| #include "commands/seclabel.h" | ||
| #include "commands/tablespace.h" | ||
| #include "commands/tag.h" | ||
| #include "common/file_perm.h" | ||
| #include "mb/pg_wchar.h" | ||
| #include "miscadmin.h" | ||
| #include "pgstat.h" | ||
|
|
@@ -2422,6 +2423,45 @@ get_database_name(Oid dbid) | |
| return result; | ||
| } | ||
|
|
||
| /* | ||
| * recovery_create_dbdir() | ||
| * | ||
| * During recovery, there's a case where we validly need to recover a missing | ||
| * tablespace directory so that recovery can continue. This happens when | ||
| * recovery wants to create a database but the holding tablespace has been | ||
| * removed before the server stopped. Since we expect that the directory will | ||
| * be gone before reaching recovery consistency, and we have no knowledge about | ||
| * the tablespace other than its OID here, we create a real directory under | ||
| * pg_tblspc here instead of restoring the symlink. | ||
| * | ||
| * If only_tblspc is true, then the requested directory must be in pg_tblspc/ | ||
| */ | ||
| static void | ||
| recovery_create_dbdir(char *path, bool only_tblspc) | ||
| { | ||
| struct stat st; | ||
|
|
||
| Assert(RecoveryInProgress()); | ||
|
|
||
| if (stat(path, &st) == 0) | ||
| return; | ||
|
|
||
| if (only_tblspc && strstr(path, "pg_tblspc/") == NULL) | ||
| elog(PANIC, "requested to created invalid directory: %s", path); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: requested to create invalid directory |
||
|
|
||
| if (reachedConsistency && !allow_in_place_tablespaces) | ||
| ereport(PANIC, | ||
| errmsg("missing directory \"%s\"", path)); | ||
|
|
||
| elog(reachedConsistency ? WARNING : DEBUG1, | ||
| "creating missing directory: %s", path); | ||
|
|
||
| if (pg_mkdir_p(path, pg_dir_create_mode) != 0) | ||
| ereport(PANIC, | ||
| errmsg("could not create missing directory \"%s\": %m", path)); | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * DATABASE resource manager's routines | ||
| */ | ||
|
|
@@ -2438,7 +2478,7 @@ dbase_redo(XLogReaderState *record) | |
| xl_dbase_create_rec *xlrec = (xl_dbase_create_rec *) XLogRecGetData(record); | ||
| char *src_path; | ||
| char *dst_path; | ||
| char *parentdir; | ||
| char *parent_path; | ||
| struct stat st; | ||
|
|
||
| src_path = GetDatabasePath(xlrec->src_db_id, xlrec->src_tablespace_id); | ||
|
|
@@ -2459,28 +2499,32 @@ dbase_redo(XLogReaderState *record) | |
| } | ||
|
|
||
| /* | ||
| * It is possible that the tablespace was later dropped, but we are | ||
| * re-redoing database create before that. In that case, | ||
| * either src_path or dst_path is probably missing here and needs to | ||
| * be created. We create directories here so that copy_dir() won't | ||
| * fail, but do not bother to create the symlink under pg_tblspc | ||
| * if the tablespace is not global/default. | ||
| * If the parent of the target path doesn't exist, create it now. This | ||
| * enables us to create the target underneath later. Note that if | ||
| * the database dir is not in a tablespace, the parent will always | ||
| * exist, so this never runs in that case. | ||
| */ | ||
| if (stat(src_path, &st) != 0 && pg_mkdir_p(src_path, S_IRWXU) != 0) | ||
| { | ||
| ereport(WARNING, | ||
| (errmsg("can not recursively create directory \"%s\"", | ||
| src_path))); | ||
| } | ||
| parentdir = pstrdup(dst_path); | ||
| get_parent_directory(parentdir); | ||
| if (stat(parentdir, &st) != 0 && pg_mkdir_p(parentdir, S_IRWXU) != 0) | ||
| parent_path = pstrdup(dst_path); | ||
| get_parent_directory(parent_path); | ||
| if (stat(parent_path, &st) < 0) | ||
| { | ||
| ereport(WARNING, | ||
| (errmsg("can not recursively create directory \"%s\"", | ||
| parentdir))); | ||
| if (errno != ENOENT) | ||
| ereport(FATAL, | ||
| errmsg("could not stat directory \"%s\": %m", | ||
| dst_path)); | ||
|
|
||
| recovery_create_dbdir(parent_path, true); | ||
| } | ||
| pfree(parentdir); | ||
| pfree(parent_path); | ||
|
|
||
| /* | ||
| * There's a case where the copy source directory is missing for the | ||
| * same reason above. Create the emtpy source directory so that | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: emtpy instead of empty |
||
| * copydir below doesn't fail. The directory will be dropped soon by | ||
| * recovery. | ||
| */ | ||
| if (stat(src_path, &st) < 0 && errno == ENOENT) | ||
| recovery_create_dbdir(src_path, false); | ||
|
|
||
| /* | ||
| * Force dirty buffers out to disk, to ensure source database is | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
|
|
||
| # Copyright (c) 2021-2022, PostgreSQL Global Development Group | ||
|
|
||
| # Test replay of tablespace/database creation/drop | ||
|
|
||
| use strict; | ||
| use warnings; | ||
|
|
||
| use PostgreSQL::Test::Cluster; | ||
| use PostgreSQL::Test::Utils; | ||
| use Test::More; | ||
|
|
||
| sub test_tablespace | ||
| { | ||
| my $node_primary = PostgreSQL::Test::Cluster->new("primary1"); | ||
| $node_primary->init(allows_streaming => 1); | ||
| $node_primary->start; | ||
| $node_primary->psql( | ||
| 'postgres', | ||
| qq[ | ||
| SET allow_in_place_tablespaces=on; | ||
| CREATE TABLESPACE dropme_ts1 LOCATION ''; | ||
| CREATE TABLESPACE dropme_ts2 LOCATION ''; | ||
| CREATE TABLESPACE source_ts LOCATION ''; | ||
| CREATE TABLESPACE target_ts LOCATION ''; | ||
| CREATE DATABASE template_db IS_TEMPLATE = true; | ||
| SELECT pg_create_physical_replication_slot('slot', true); | ||
| ]); | ||
| my $backup_name = 'my_backup'; | ||
| $node_primary->backup($backup_name); | ||
|
|
||
| my $node_standby = PostgreSQL::Test::Cluster->new("standby2"); | ||
| $node_standby->init_from_backup($node_primary, $backup_name, | ||
| has_streaming => 1); | ||
| $node_standby->append_conf('postgresql.conf', | ||
| "allow_in_place_tablespaces = on"); | ||
| $node_standby->start; | ||
|
|
||
| # Make sure connection is made | ||
| $node_primary->poll_query_until('postgres', | ||
| 'SELECT count(*) = 1 FROM pg_stat_replication'); | ||
| $node_primary->safe_psql('postgres', "SELECT pg_drop_replication_slot('slot')"); | ||
|
|
||
| $node_standby->safe_psql('postgres', 'CHECKPOINT'); | ||
|
|
||
| # Do immediate shutdown just after a sequence of CREATE DATABASE / DROP | ||
| # DATABASE / DROP TABLESPACE. This causes CREATE DATABASE WAL records | ||
| # to be applied to already-removed directories. | ||
| my $query = q[ | ||
| CREATE DATABASE dropme_db1 WITH TABLESPACE dropme_ts1; | ||
| CREATE TABLE t (a int) TABLESPACE dropme_ts2; | ||
| CREATE DATABASE dropme_db2 WITH TABLESPACE dropme_ts2; | ||
| CREATE DATABASE moveme_db TABLESPACE source_ts; | ||
| ALTER DATABASE moveme_db SET TABLESPACE target_ts; | ||
| CREATE DATABASE newdb TEMPLATE template_db; | ||
| ALTER DATABASE template_db IS_TEMPLATE = false; | ||
| DROP DATABASE dropme_db1; | ||
| DROP TABLE t; | ||
| DROP DATABASE dropme_db2; DROP TABLESPACE dropme_ts2; | ||
| DROP TABLESPACE source_ts; | ||
| DROP DATABASE template_db; | ||
| ]; | ||
|
|
||
| $node_primary->safe_psql('postgres', $query); | ||
| $node_primary->wait_for_catchup($node_standby, 'replay', | ||
| $node_primary->lsn('write')); | ||
|
|
||
| # show "create missing directory" log message | ||
| $node_standby->safe_psql('postgres', | ||
| "ALTER SYSTEM SET log_min_messages TO debug1;"); | ||
| $node_standby->stop('immediate'); | ||
| # Should restart ignoring directory creation error. | ||
| is($node_standby->start(fail_ok => 1), 1, "standby node started"); | ||
| $node_standby->stop('immediate'); | ||
| } | ||
|
|
||
| test_tablespace(); | ||
|
|
||
| # Ensure that a missing tablespace directory during create database | ||
| # replay immediately causes panic if the standby has already reached | ||
| # consistent state (archive recovery is in progress). | ||
|
|
||
| my $node_primary = PostgreSQL::Test::Cluster->new('primary2'); | ||
| $node_primary->init(allows_streaming => 1); | ||
| $node_primary->start; | ||
|
|
||
| # Create tablespace | ||
| $node_primary->safe_psql( | ||
| 'postgres', q[ | ||
| SET allow_in_place_tablespaces=on; | ||
| CREATE TABLESPACE ts1 LOCATION '' | ||
| ]); | ||
| $node_primary->safe_psql('postgres', | ||
| "CREATE DATABASE db1 WITH TABLESPACE ts1"); | ||
|
|
||
| # Take backup | ||
| my $backup_name = 'my_backup'; | ||
| $node_primary->backup($backup_name); | ||
| my $node_standby = PostgreSQL::Test::Cluster->new('standby3'); | ||
| $node_standby->init_from_backup($node_primary, $backup_name, | ||
| has_streaming => 1); | ||
| $node_standby->append_conf('postgresql.conf', | ||
| "allow_in_place_tablespaces = on"); | ||
| $node_standby->start; | ||
|
|
||
| # Make sure standby reached consistency and starts accepting connections | ||
| $node_standby->poll_query_until('postgres', 'SELECT 1', '1'); | ||
|
|
||
| # Remove standby tablespace directory so it will be missing when | ||
| # replay resumes. | ||
| my $tspoid = $node_standby->safe_psql('postgres', | ||
| "SELECT oid FROM pg_tablespace WHERE spcname = 'ts1';"); | ||
| my $tspdir = $node_standby->data_dir . "/pg_tblspc/$tspoid"; | ||
| File::Path::rmtree($tspdir); | ||
|
|
||
| my $logstart = get_log_size($node_standby); | ||
|
|
||
| # Create a database in the tablespace and a table in default tablespace | ||
| $node_primary->safe_psql( | ||
| 'postgres', | ||
| q[ | ||
| CREATE TABLE should_not_replay_insertion(a int); | ||
| CREATE DATABASE db2 WITH TABLESPACE ts1; | ||
| INSERT INTO should_not_replay_insertion VALUES (1); | ||
| ]); | ||
|
|
||
| # Standby should fail and should not silently skip replaying the wal | ||
| # In this test, PANIC turns into WARNING by allow_in_place_tablespaces. | ||
| # Check the log messages instead of confirming standby failure. | ||
| my $max_attempts = $PostgreSQL::Test::Utils::timeout_default; | ||
| while ($max_attempts-- >= 0) | ||
| { | ||
| last | ||
| if ( | ||
| find_in_log( | ||
| $node_standby, "WARNING: creating missing directory: pg_tblspc/", | ||
| $logstart)); | ||
| sleep 1; | ||
| } | ||
| ok($max_attempts > 0, "invalid directory creation is detected"); | ||
|
|
||
| done_testing(); | ||
|
|
||
|
|
||
| # return the size of logfile of $node in bytes | ||
| sub get_log_size | ||
| { | ||
| my ($node) = @_; | ||
|
|
||
| return (stat $node->logfile)[7]; | ||
| } | ||
|
|
||
| # find $pat in logfile of $node after $off-th byte | ||
| sub find_in_log | ||
| { | ||
| my ($node, $pat, $off) = @_; | ||
|
|
||
| $off = 0 unless defined $off; | ||
| my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile); | ||
| return 0 if (length($log) <= $off); | ||
|
|
||
| $log = substr($log, $off); | ||
|
|
||
| return $log =~ m/$pat/; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: fictitious