diff --git a/src/backend/storage/ipc/dsm.c b/src/backend/storage/ipc/dsm.c index da1f32659e..bd0727f445 100644 --- a/src/backend/storage/ipc/dsm.c +++ b/src/backend/storage/ipc/dsm.c @@ -44,6 +44,7 @@ #include "storage/pg_shmem.h" #include "storage/shmem.h" #include "utils/freepage.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/resowner.h" @@ -751,6 +752,7 @@ dsm_attach(dsm_handle h) */ PG_TRY(); { + INJECTION_POINT("dsm-attach-before-mapping", NULL); dsm_impl_op(DSM_OP_ATTACH, seg->handle, 0, &seg->impl_private, &seg->mapped_address, &seg->mapped_size, ERROR); } diff --git a/src/test/modules/injection_points/injection_points--1.0.sql b/src/test/modules/injection_points/injection_points--1.0.sql index cc76b1bf99..ffced61211 100644 --- a/src/test/modules/injection_points/injection_points--1.0.sql +++ b/src/test/modules/injection_points/injection_points--1.0.sql @@ -110,6 +110,24 @@ RETURNS record AS 'MODULE_PATHNAME', 'injection_points_stats_fixed' LANGUAGE C STRICT; +-- +-- Test-only pgstats helpers +-- +CREATE FUNCTION injection_points_create_pgstats(IN nentries int) +RETURNS oid +AS 'MODULE_PATHNAME', 'injection_points_create_pgstats' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION injection_points_lookup_pgstats(IN relid oid) +RETURNS bool +AS 'MODULE_PATHNAME', 'injection_points_lookup_pgstats' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION injection_points_test_pgstats_dsm_recovery(IN relid oid) +RETURNS bool +AS 'MODULE_PATHNAME', 'injection_points_test_pgstats_dsm_recovery' +LANGUAGE C STRICT PARALLEL UNSAFE; + -- -- regress_injection.c functions -- diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c index c493cfda9c..dff752ecd1 100644 --- a/src/test/modules/injection_points/injection_points.c +++ b/src/test/modules/injection_points/injection_points.c @@ -17,6 +17,7 @@ #include "postgres.h" +#include "access/xact.h" #include "fmgr.h" #include "injection_points.h" #include "injection_stats.h" @@ -32,6 +33,8 @@ #include "utils/guc.h" #include "utils/injection_point.h" #include "utils/memutils.h" +#include "utils/pgstat_internal.h" +#include "utils/resowner.h" #include "utils/wait_event.h" PG_MODULE_MAGIC; @@ -46,6 +49,9 @@ PG_MODULE_MAGIC; */ static List *inj_list_local = NIL; +#define PGSTATS_DSM_TEST_FIRST_RELID ((Oid) 800000000) +#define PGSTATS_DSM_TEST_INJECTION_POINT "dsm-attach-before-mapping" + /* * Shared state information for injection points. * @@ -80,6 +86,8 @@ extern PGDLLEXPORT void injection_wait(const char *name, const void *private_data, void *arg); +static void injection_points_detach_by_name(char *name); + /* track if injection points attached in this process are linked to it */ static bool injection_point_local = false; @@ -517,6 +525,14 @@ injection_points_detach(PG_FUNCTION_ARGS) { char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + injection_points_detach_by_name(name); + + PG_RETURN_VOID(); +} + +static void +injection_points_detach_by_name(char *name) +{ pgstat_report_inj_fixed(0, 1, 0, 0, 0); if (!InjectionPointDetach(name)) elog(ERROR, "could not detach injection point \"%s\"", name); @@ -533,8 +549,106 @@ injection_points_detach(PG_FUNCTION_ARGS) /* Remove stats entry */ pgstat_drop_inj(name); +} - PG_RETURN_VOID(); +PG_FUNCTION_INFO_V1(injection_points_create_pgstats); +Datum +injection_points_create_pgstats(PG_FUNCTION_ARGS) +{ + int32 nentries = PG_GETARG_INT32(0); + Oid target = InvalidOid; + + if (nentries <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("number of pgstats entries must be positive"))); + + for (int32 i = 0; i < nentries; i++) + { + PgStat_EntryRef *entry_ref; + Oid relid = PGSTATS_DSM_TEST_FIRST_RELID + i; + + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_RELATION, + MyDatabaseId, relid, true, NULL); + + if (target == InvalidOid && + (entry_ref->shared_entry->body >> DSA_OFFSET_WIDTH) > 0) + target = relid; + } + + if (target == InvalidOid) + ereport(ERROR, + errmsg("could not create a pgstats entry in a DSM-backed DSA segment")); + + PG_RETURN_OID(target); +} + +PG_FUNCTION_INFO_V1(injection_points_lookup_pgstats); +Datum +injection_points_lookup_pgstats(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + PgStat_EntryRef *entry_ref; + + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_RELATION, + MyDatabaseId, relid, false, NULL); + + PG_RETURN_BOOL(entry_ref != NULL); +} + +PG_FUNCTION_INFO_V1(injection_points_test_pgstats_dsm_recovery); +Datum +injection_points_test_pgstats_dsm_recovery(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + bool caught = false; + MemoryContext oldcontext = CurrentMemoryContext; + ResourceOwner oldowner = CurrentResourceOwner; + + BeginInternalSubTransaction(NULL); + + PG_TRY(); + { + (void) pgstat_get_entry_ref(PGSTAT_KIND_RELATION, + MyDatabaseId, relid, false, NULL); + ReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(oldcontext); + CurrentResourceOwner = oldowner; + } + PG_CATCH(); + { + ErrorData *edata; + + MemoryContextSwitchTo(oldcontext); + edata = CopyErrorData(); + FlushErrorState(); + + RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(oldcontext); + CurrentResourceOwner = oldowner; + + if (edata->message == NULL || + strstr(edata->message, PGSTATS_DSM_TEST_INJECTION_POINT) == NULL) + ReThrowError(edata); + + caught = true; + FreeErrorData(edata); + } + PG_END_TRY(); + + if (!caught) + ereport(ERROR, + errmsg("expected DSM attach injection point to raise an error")); + + injection_points_detach_by_name(PGSTATS_DSM_TEST_INJECTION_POINT); + pgstat_request_entry_refs_gc(); + + if (pgstat_get_entry_ref(PGSTAT_KIND_RELATION, + MyDatabaseId, relid, false, NULL) == NULL) + ereport(ERROR, + errmsg("could not find pgstats entry after DSM attach recovery")); + + PG_RETURN_BOOL(true); } diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 2eec3d0935..27d333a35e 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -60,6 +60,7 @@ tests += { }, 'tests': [ 't/001_stats.pl', + 't/002_pgstats_dsm.pl', ], }, } diff --git a/src/test/modules/injection_points/t/002_pgstats_dsm.pl b/src/test/modules/injection_points/t/002_pgstats_dsm.pl new file mode 100644 index 0000000000..b86abd84e7 --- /dev/null +++ b/src/test/modules/injection_points/t/002_pgstats_dsm.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test recovery after an ERROR while lazily attaching a DSM segment backing +# the pgstats DSA. +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +min_dynamic_shared_memory = 0 +)); +$node->start; + +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); + +my $tester = $node->background_psql('postgres'); +$tester->set_query_timer_restart(); + +# Resolve the test functions before the filler backend creates a DSM-backed +# pgstats DSA segment that this backend has not mapped. +$tester->query_safe("SELECT injection_points_lookup_pgstats('0'::oid)"); +$tester->query_safe( + "PREPARE pgstats_dsm_recovery(oid) AS SELECT injection_points_test_pgstats_dsm_recovery(\$1)" +); + +$tester->query_safe('SELECT injection_points_set_local()'); +$tester->query_safe( + "SELECT injection_points_attach('dsm-attach-before-mapping', 'error')"); + +my $target_relid = $node->safe_psql('postgres', + 'SELECT injection_points_create_pgstats(10000)'); +like($target_relid, qr/^[0-9]+$/, + 'created pgstats entry in DSM-backed DSA segment'); + +my $result = + $tester->query_safe("EXECUTE pgstats_dsm_recovery('$target_relid'::oid)"); +is($result, 't', 'pgstats lookup recovers after injected DSM attach ERROR'); + +$tester->quit; +$node->stop; + +done_testing();