test-WAL_LOG-chkpt-interleave-v0.patch
text/plain
Filename: test-WAL_LOG-chkpt-interleave-v0.patch
Type: text/plain
Part: 2
Patch
Format: unified
Series: patch v0
| File | + | − |
|---|---|---|
| src/backend/backup/basebackup.c | 3 | 0 |
| src/backend/commands/dbcommands.c | 3 | 0 |
| src/backend/tcop/postgres.c | 1 | 0 |
| src/backend/utils/cache/relmapper.c | 2 | 0 |
| src/backend/utils/misc/injection_point.c | 33 | 1 |
| src/backend/utils/mmgr/mcxt.c | 14 | 3 |
| src/include/utils/injection_point.h | 1 | 0 |
| src/include/utils/memutils.h | 2 | 0 |
| src/test/modules/injection_points/injection_points.c | 17 | 1 |
| src/test/modules/injection_points/Makefile | 2 | 0 |
| src/test/modules/injection_points/meson.build | 5 | 0 |
| src/test/modules/injection_points/t/001_create_database.pl | 70 | 0 |
| src/test/perl/PostgreSQL/Test/BackgroundPsql.pm | 23 | 0 |
| src/test/perl/PostgreSQL/Test/Cluster.pm | 21 | 0 |
Author: Noah Misch <noah@leadboat.com>
Commit: Noah Misch <noah@leadboat.com>
[not for commit] Demonstrate bugs that WAL_LOG-chkpt-interleave fixes.
Components that might be separate patches if committing:
- Allow injection points in critical sections
- BackgroundPsql: add feature to wait for stderr content, not just stdout
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index d5b8ca2..943bbb4 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -45,6 +45,7 @@
#include "storage/reinit.h"
#include "utils/builtins.h"
#include "utils/guc.h"
+#include "utils/injection_point.h"
#include "utils/ps_status.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
@@ -994,6 +995,8 @@ SendBaseBackup(BaseBackupCmd *cmd, IncrementalBackupInfo *ib)
bbsink *sink;
SessionBackupState status = get_backup_status();
+ INJECTION_POINT("begin-SendBaseBackup");
+
if (status == SESSION_BACKUP_RUNNING)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 0a97a11..f9bc53c 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -65,6 +65,7 @@
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
+#include "utils/injection_point.h"
#include "utils/pg_locale.h"
#include "utils/relmapper.h"
#include "utils/snapmgr.h"
@@ -486,6 +487,8 @@ CreateDirAndVersionFile(char *dbpath, Oid dbid, Oid tsid, bool isRedo)
sizeof(xl_dbase_create_wal_log_rec));
lsn = XLogInsert(RM_DBASE_ID, XLOG_DBASE_CREATE_WAL_LOG);
+ /* at merge conflict w/ fix patch, keep this with the XLogInsert() */
+ INJECTION_POINT("after-XLOG_DBASE_CREATE_WAL_LOG");
/* As always, WAL must hit the disk before the data update does. */
XLogFlush(lsn);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3..226814e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
#include "tcop/tcopprot.h"
#include "tcop/utility.h"
#include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
diff --git a/src/backend/utils/cache/relmapper.c b/src/backend/utils/cache/relmapper.c
index 58807f7..f5c70b7 100644
--- a/src/backend/utils/cache/relmapper.c
+++ b/src/backend/utils/cache/relmapper.c
@@ -53,6 +53,7 @@
#include "pgstat.h"
#include "storage/fd.h"
#include "storage/lwlock.h"
+#include "utils/injection_point.h"
#include "utils/inval.h"
#include "utils/relmapper.h"
@@ -962,6 +963,7 @@ write_relmap_file(RelMapFile *newmap, bool write_wal, bool send_sinval,
XLogRegisterData((char *) newmap, sizeof(RelMapFile));
lsn = XLogInsert(RM_RELMAP_ID, XLOG_RELMAP_UPDATE);
+ INJECTION_POINT("after-XLOG_RELMAP_UPDATE");
/* As always, WAL must hit the disk before the data update does */
XLogFlush(lsn);
diff --git a/src/backend/utils/misc/injection_point.c b/src/backend/utils/misc/injection_point.c
index 0cf4d51..1fd63ea 100644
--- a/src/backend/utils/misc/injection_point.c
+++ b/src/backend/utils/misc/injection_point.c
@@ -232,6 +232,26 @@ InjectionPointAttach(const char *name,
}
/*
+ * Probe for an existing injection point.
+ */
+bool
+InjectionPointAttached(const char *name)
+{
+#ifdef USE_INJECTION_POINTS
+ bool found;
+
+ LWLockAcquire(InjectionPointLock, LW_SHARED);
+ (void) hash_search(InjectionPointHash, name,
+ HASH_FIND, &found);
+ LWLockRelease(InjectionPointLock);
+
+ return found;
+#else
+ elog(ERROR, "Injection points are not supported by this build");
+#endif
+}
+
+/*
* Detach an existing injection point.
*/
void
@@ -262,10 +282,17 @@ void
InjectionPointRun(const char *name)
{
#ifdef USE_INJECTION_POINTS
+ bool reset_allow = false;
InjectionPointEntry *entry_by_name;
bool found;
InjectionPointCallback injection_callback;
+ if (CritSectionCount > 0 && !MemoryContextAllowAllInCriticalSection)
+ {
+ reset_allow = true;
+ MemoryContextAllowAllInCriticalSection = true;
+ }
+
LWLockAcquire(InjectionPointLock, LW_SHARED);
entry_by_name = (InjectionPointEntry *)
hash_search(InjectionPointHash, name,
@@ -279,7 +306,7 @@ InjectionPointRun(const char *name)
if (!found)
{
injection_point_cache_remove(name);
- return;
+ goto out;
}
/*
@@ -311,6 +338,11 @@ InjectionPointRun(const char *name)
}
injection_callback(name);
+
+out:
+ /* elog(ERROR) would have become PANIC, so we never miss this reset */
+ if (reset_allow)
+ MemoryContextAllowAllInCriticalSection = false;
#else
elog(ERROR, "Injection points are not supported by this build");
#endif
diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c
index 1336944..3a57c6a 100644
--- a/src/backend/utils/mmgr/mcxt.c
+++ b/src/backend/utils/mmgr/mcxt.c
@@ -149,6 +149,17 @@ MemoryContext CurTransactionContext = NULL;
/* This is a transient link to the active portal's memory context: */
MemoryContext PortalContext = NULL;
+/*
+ * If true, suppress all assertions about allocations in critical sections.
+ * Behave as though MemoryContextAllowInCriticalSection() had been called for
+ * every context, and permit new contexts. Caller is responsible for
+ * restoring value on error. The intended use case is debugging aids that
+ * can't use a single-purpose context. For example, LockAcquire() allocates
+ * in TopMemoryContext, so a debugging aid that calls it has no clean way to
+ * redirect that allocation.
+ */
+bool MemoryContextAllowAllInCriticalSection = false;
+
static void MemoryContextCallResetCallbacks(MemoryContext context);
static void MemoryContextStatsInternal(MemoryContext context, int level,
bool print, int max_children,
@@ -164,7 +175,8 @@ static void MemoryContextStatsPrint(MemoryContext context, void *passthru,
* rule, the allocation functions Assert that.
*/
#define AssertNotInCriticalSection(context) \
- Assert(CritSectionCount == 0 || (context)->allowInCritSection)
+ Assert(CritSectionCount == 0 || (context)->allowInCritSection || \
+ MemoryContextAllowAllInCriticalSection)
/*
* Call the given function in the MemoryContextMethods for the memory context
@@ -976,8 +988,7 @@ MemoryContextCreate(MemoryContext node,
MemoryContext parent,
const char *name)
{
- /* Creating new memory contexts is not allowed in a critical section */
- Assert(CritSectionCount == 0);
+ Assert(CritSectionCount == 0 || MemoryContextAllowAllInCriticalSection);
/* Initialize all standard fields of memory context header */
node->type = tag;
diff --git a/src/include/utils/injection_point.h b/src/include/utils/injection_point.h
index 55524b5..57d9f3a 100644
--- a/src/include/utils/injection_point.h
+++ b/src/include/utils/injection_point.h
@@ -31,6 +31,7 @@ extern void InjectionPointShmemInit(void);
extern void InjectionPointAttach(const char *name,
const char *library,
const char *function);
+extern bool InjectionPointAttached(const char *name);
extern void InjectionPointRun(const char *name);
extern void InjectionPointDetach(const char *name);
diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h
index ca7858d..ab46aaf 100644
--- a/src/include/utils/memutils.h
+++ b/src/include/utils/memutils.h
@@ -90,6 +90,8 @@ extern void MemoryContextStatsDetail(MemoryContext context, int max_children,
extern void MemoryContextAllowInCriticalSection(MemoryContext context,
bool allow);
+extern PGDLLIMPORT bool MemoryContextAllowAllInCriticalSection;
+
#ifdef MEMORY_CONTEXT_CHECKING
extern void MemoryContextCheck(MemoryContext context);
#endif
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cbbae4..3ca6859 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -8,6 +8,8 @@ PGFILEDESC = "injection_points - facility for injection points"
REGRESS = injection_points
+TAP_TESTS = 1
+
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c
index e843e65..feff3f1 100644
--- a/src/test/modules/injection_points/injection_points.c
+++ b/src/test/modules/injection_points/injection_points.c
@@ -18,6 +18,7 @@
#include "postgres.h"
#include "fmgr.h"
+#include "miscadmin.h"
#include "storage/lwlock.h"
#include "storage/shmem.h"
#include "utils/builtins.h"
@@ -28,10 +29,23 @@ PG_MODULE_MAGIC;
extern PGDLLEXPORT void injection_error(const char *name);
extern PGDLLEXPORT void injection_notice(const char *name);
+extern PGDLLEXPORT void injection_await_detach(const char *name);
/* Set of callbacks available to be attached to an injection point. */
void
+injection_await_detach(const char *name)
+{
+ elog(WARNING, "injection point waiting for detach of %s", name);
+ while (InjectionPointAttached(name))
+ {
+ CHECK_FOR_INTERRUPTS();
+ pg_usleep(10 * 1000);
+ }
+ elog(WARNING, "injection point observed detach of %s", name);
+}
+
+void
injection_error(const char *name)
{
elog(ERROR, "error triggered for injection point %s", name);
@@ -54,7 +68,9 @@ injection_points_attach(PG_FUNCTION_ARGS)
char *action = text_to_cstring(PG_GETARG_TEXT_PP(1));
char *function;
- if (strcmp(action, "error") == 0)
+ if (strcmp(action, "await-detach") == 0)
+ function = "injection_await_detach";
+ else if (strcmp(action, "error") == 0)
function = "injection_error";
else if (strcmp(action, "notice") == 0)
function = "injection_notice";
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index ee37573..1e2adbc 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -34,4 +34,9 @@ tests += {
'injection_points',
],
},
+ 'tap': {
+ 'tests': [
+ 't/001_create_database.pl',
+ ],
+ },
}
diff --git a/src/test/modules/injection_points/t/001_create_database.pl b/src/test/modules/injection_points/t/001_create_database.pl
new file mode 100644
index 0000000..47d1299
--- /dev/null
+++ b/src/test/modules/injection_points/t/001_create_database.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Test usability of a base backup taken after a particular WAL record.
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+foreach
+ my $point ('after-XLOG_RELMAP_UPDATE', 'after-XLOG_DBASE_CREATE_WAL_LOG')
+{
+ # create a base backup at $point, a post-XLogInsert injection point
+ my $origin = PostgreSQL::Test::Cluster->new('origin-' . $point);
+ $origin->init(has_archiving => 1, allows_streaming => 1);
+ $origin->start;
+ my $creator = $origin->background_psql('postgres');
+ my $injector = $origin->background_psql('postgres');
+ $injector->query_safe(<<EOSQL);
+ CREATE EXTENSION injection_points;
+ SELECT injection_points_attach(ip, 'await-detach')
+ FROM (VALUES ('$point'), ('begin-SendBaseBackup')) t(ip);
+EOSQL
+ # Start the backup until it pauses at begin-SendBaseBackup.
+ my $backup_client =
+ $origin->backup_async($point, backup_options => ['--wal-method=none']);
+ # FIXME replace sleep w/ watch for "injection point waiting", perhaps by
+ # introducing "->backup(until => qr/regexp/)".
+ sleep 1;
+ # Run CREATE DATABASE, which stops at $point. RelationMappingLock blocks
+ # InitPostgres(), so start all backends before here. We used
+ # --wal-method=none above since the default would start an additional
+ # connection too late. (The archive makes it superfluous to acquire WAL
+ # through pg_basebackup.)
+ $creator->query_until_stderr(qr/waiting.*$point/,
+ "CREATE DATABASE regress_created;\n");
+ $injector->query_safe(
+ "SELECT injection_points_detach('begin-SendBaseBackup');");
+ # FIXME While this is an effective test of XLOG_DBASE_CREATE_WAL_LOG, the
+ # XLOG_RELMAP_UPDATE makes the backup's checkpoint hang waiting for
+ # RelationMappingLock. To have a test that both fails with the bug and
+ # passes with its fix, we'd need a procedure like this:
+ #
+ # while (backup client still not done)
+ # {
+ # run backup client until it's waiting on a lock CREATE DATABASE holds;
+ # unpause CREATE DATABASE and re-pause it after its next lock release;
+ # }
+ $backup_client->finish;
+ $injector->query_safe("SELECT injection_points_detach('$point');");
+ $injector->quit;
+ $creator->quit;
+ $origin->stop;
+
+ # replay backup to the end; confirm new db accepts connections
+ my $replica = PostgreSQL::Test::Cluster->new('replica-' . $point);
+ $replica->init_from_backup(
+ $origin, $point,
+ has_restoring => 1,
+ standby => 0);
+ $replica->start;
+ $replica->poll_query_until('postgres',
+ "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for recovery to end";
+ my $rc = $replica->psql('regress_created', 'SELECT 1');
+ is($rc, 0, "backup @ $point usable");
+ $replica->stop;
+}
+
+done_testing();
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 4091c31..2b9f26d 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -288,6 +288,29 @@ sub query_until
return $ret;
}
+# FIXME perhaps make "query_until" monitor both stdout and stderr, then remove
+# this fork.
+sub query_until_stderr
+{
+ my ($self, $until, $query) = @_;
+ my $ret;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ $self->{timeout}->start() if (defined($self->{query_timer_restart}));
+ $self->{stdin} .= $query;
+
+ pump_until($self->{run}, $self->{timeout}, \$self->{stderr}, $until);
+
+ die "psql query timed out" if $self->{timeout}->is_expired;
+
+ $ret = $self->{stderr};
+
+ # clear out output for the next query
+ $self->{stderr} = '';
+
+ return $ret;
+}
+
=pod
=item $session->set_query_timer_restart()
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e2e70d0..3ff0df9 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -739,6 +739,27 @@ sub backup
return;
}
+# FIXME
+sub backup_async
+{
+ my ($self, $backup_name, %params) = @_;
+ my $backup_path = $self->backup_dir . '/' . $backup_name;
+ my $name = $self->name;
+
+ local %ENV = $self->_get_env();
+
+ print "# Taking pg_basebackup $backup_name from node \"$name\"\n";
+ return IPC::Run::start(
+ [
+ 'pg_basebackup', '-D',
+ $backup_path, '-h',
+ $self->host, '-p',
+ $self->port, '--checkpoint',
+ 'fast', '--no-sync',
+ @{ $params{backup_options} }
+ ]);
+}
+
=item $node->backup_fs_cold(backup_name)
Create a backup with a filesystem level copy in subdirectory B<backup_name> of