Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1rD0GI-000Zvq-P7 for pgsql-hackers@arkaria.postgresql.org; Tue, 12 Dec 2023 10:45:15 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1rD0GG-003XEn-3f for pgsql-hackers@arkaria.postgresql.org; Tue, 12 Dec 2023 10:45:12 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1rD0GE-003XEe-0l for pgsql-hackers@lists.postgresql.org; Tue, 12 Dec 2023 10:45:11 +0000 Received: from wout5-smtp.messagingengine.com ([64.147.123.21]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1rD0G7-00A1AN-9r for pgsql-hackers@lists.postgresql.org; Tue, 12 Dec 2023 10:45:08 +0000 Received: from compute5.internal (compute5.nyi.internal [10.202.2.45]) by mailout.west.internal (Postfix) with ESMTP id 48C4F3200A63; Tue, 12 Dec 2023 05:45:01 -0500 (EST) Received: from mailfrontend1 ([10.202.2.162]) by compute5.internal (MEProxy); Tue, 12 Dec 2023 05:45:01 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=paquier.xyz; h= cc:cc:content-type:content-type:date:date:from:from:in-reply-to :in-reply-to:message-id:mime-version:references:reply-to:subject :subject:to:to; s=fm1; t=1702377900; x=1702464300; bh=A5ctBSTP8k ZIAwqRXyOUb3e2O9wx1A+RH4n9r4WhzoU=; b=YTJJgW1rJ1xW0p7mQzDyeFJjlG EjKv3Uf6qSJzXpyWCex3Eu/Wx/VwejktkJ/zMHmaNSYGe2YR8OO+i9rhAOGJIdzv k5GEVblx6K4r39QrTjH2oOd2g+losb8WG9Zfr59Z7T6UiBDMMlqsCFL5a9NIlN23 D6/RByRjJUV+VojxcdDwPjnBErYRevURdEcttnlG3o3a/2zvQ5gMTZxD0r7IY4jh WKOCzJPleDvxQ1I2I91BFXZyF52ipVEbD7C4aBpmrlY67i90/HMCufszVYcA8ylY BXFdHps8ainpNGrR0FG8vVwZocKSz20VxHD6PjSQL+BcXpXIXjVs3fJtcOYQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm1; t=1702377900; x=1702464300; bh=A5ctBSTP8kZIAwqRXyOUb3e2O9wx 1A+RH4n9r4WhzoU=; b=VeOE0dPK5j2VV1RACwAJP0KCDMc4+PhyK2NuJNYiB30q 82xLPjQS9p/mxr+40MHrvEDbv9tCDR8B+w7T8eYRYoMaVNmJ//PIEEfJqvMvRDFa SCEBUdxRkwe5ZfVg0UHIXaNUcqrN5zGNqUNaYlzR2SvgCMngg5Wh1uVOcI7PHnGf temHhTvNzhTwWZQcc8qfc6SgovlAWUWKYBr4D1vMKQKQPNO8JCToo+MwJkAUSbfE J2sHDw09+ILadCU2almsyuO0cEai04tzkKkd5h/hW1MDBnrwiYsw/iSja2WZFgv2 eiKih2Qah0GeTEgcxahrdhd3Ev4/MZf59cIGpd2gNA== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvkedrudelgedgudekucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmne gfrhhlucfvnfffucdljedtmdenucfjughrpeffhffvvefukfhfgggtuggjsehgtderredt tddvnecuhfhrohhmpefoihgthhgrvghlucfrrghquhhivghruceomhhitghhrggvlhesph grqhhuihgvrhdrgiihiieqnecuggftrfgrthhtvghrnhepteelieefudffhffhtdetleeg geegfffhkeeuveetiefgudduvedutefggeeivdejnecuvehluhhsthgvrhfuihiivgeptd enucfrrghrrghmpehmrghilhhfrhhomhepmhhitghhrggvlhesphgrqhhuihgvrhdrgiih ii X-ME-Proxy: Feedback-ID: i0fe9450f:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Tue, 12 Dec 2023 05:44:59 -0500 (EST) Date: Tue, 12 Dec 2023 11:44:57 +0100 From: Michael Paquier To: Dilip Kumar Cc: Ashutosh Bapat , Postgres hackers Subject: Re: Adding facility for injection points (or probe points?) for more advanced tests Message-ID: References: MIME-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha256; protocol="application/pgp-signature"; boundary="h3Jod0fYfP9xsIu0" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --h3Jod0fYfP9xsIu0 Content-Type: multipart/mixed; boundary="GwmmnaKI4J/fD1SE" Content-Disposition: inline --GwmmnaKI4J/fD1SE Content-Type: text/plain; charset=us-ascii Content-Disposition: inline On Tue, Dec 12, 2023 at 10:27:09AM +0530, Dilip Kumar wrote: > Oops, I only included the code changes where I am adding injection > points and some comments to verify that, but missed the actual test > file. Attaching it here. I see. Interesting that this requires persistent connections to work. That's something I've found clunky to rely on when the scenarios a test needs to deal with are rather complex. That's an area that could be made easier to use outside of this patch.. Something got proposed by Andrew Dunstan to make the libpq routines usable through a perl module, for example. > Note: I think the latest patches are conflicting with the head, can you rebase? Indeed, as per the recent manipulations in ipci.c for the shmem initialization areas. Here goes a v6. -- Michael --GwmmnaKI4J/fD1SE Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="v6-0001-Add-backend-facility-for-injection-points.patch" Content-Transfer-Encoding: quoted-printable =46rom 1e11c0400bf802834792f3e9c897b17a3d14bca1 Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Tue, 12 Dec 2023 11:35:24 +0100 Subject: [PATCH v6 1/4] Add backend facility for injection points This adds a set of routines allowing developers to attach, detach and run custom code based on arbitrary code paths set with a centralized macro called INJECTION_POINT(). Injection points are registered in a shared hash table. Processes also use a local cache to over loading callbacks more than necessary, cleaning up their cache if a callback has found to be removed. --- src/include/pg_config.h.in | 3 + src/include/utils/injection_point.h | 36 ++ src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/lmgr/lwlocknames.txt | 1 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/misc/Makefile | 1 + src/backend/utils/misc/injection_point.c | 317 ++++++++++++++++++ src/backend/utils/misc/meson.build | 1 + doc/src/sgml/installation.sgml | 30 ++ doc/src/sgml/xfunc.sgml | 56 ++++ configure | 34 ++ configure.ac | 7 + meson.build | 1 + meson_options.txt | 3 + src/Makefile.global.in | 1 + src/tools/pgindent/typedefs.list | 2 + 16 files changed, 497 insertions(+) create mode 100644 src/include/utils/injection_point.h create mode 100644 src/backend/utils/misc/injection_point.c diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 5f16918243..288bb9cb42 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -698,6 +698,9 @@ /* Define to build with ICU support. (--with-icu) */ #undef USE_ICU =20 +/* Define to 1 to build with injection points. (--enable-injection-points)= */ +#undef USE_INJECTION_POINTS + /* Define to 1 to build with LDAP support. (--with-ldap) */ #undef USE_LDAP =20 diff --git a/src/include/utils/injection_point.h b/src/include/utils/inject= ion_point.h new file mode 100644 index 0000000000..6335260fea --- /dev/null +++ b/src/include/utils/injection_point.h @@ -0,0 +1,36 @@ +/*------------------------------------------------------------------------- + * injection_point.h + * Definitions related to injection points. + * + * Copyright (c) 2001-2023, PostgreSQL Global Development Group + * + * src/include/utils/injection_point.h + * ---------- + */ +#ifndef INJECTION_POINT_H +#define INJECTION_POINT_H + +/* + * Injections points require --enable-injection-points. + */ +#ifdef USE_INJECTION_POINTS +#define INJECTION_POINT(name) InjectionPointRun(name) +#else +#define INJECTION_POINT(name) ((void) name) +#endif + +/* + * Typedef for callback function launched by an injection point. + */ +typedef void (*InjectionPointCallback) (const char *name); + +extern Size InjectionPointShmemSize(void); +extern void InjectionPointShmemInit(void); + +extern void InjectionPointAttach(const char *name, + const char *library, + const char *function); +extern void InjectionPointRun(const char *name); +extern void InjectionPointDetach(const char *name); + +#endif /* INJECTION_POINT_H */ diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 0e0ac22bdd..81799c5688 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -49,6 +49,7 @@ #include "storage/sinvaladt.h" #include "storage/spin.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/snapmgr.h" #include "utils/wait_event.h" =20 @@ -147,6 +148,7 @@ CalculateShmemSize(int *num_semaphores) size =3D add_size(size, AsyncShmemSize()); size =3D add_size(size, StatsShmemSize()); size =3D add_size(size, WaitEventExtensionShmemSize()); + size =3D add_size(size, InjectionPointShmemSize()); #ifdef EXEC_BACKEND size =3D add_size(size, ShmemBackendArraySize()); #endif @@ -348,6 +350,7 @@ CreateOrAttachShmemStructs(void) AsyncShmemInit(); StatsShmemInit(); WaitEventExtensionShmemInit(); + InjectionPointShmemInit(); } =20 /* diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage= /lmgr/lwlocknames.txt index f72f2906ce..42a048746d 100644 --- a/src/backend/storage/lmgr/lwlocknames.txt +++ b/src/backend/storage/lmgr/lwlocknames.txt @@ -54,3 +54,4 @@ XactTruncationLock 44 WrapLimitsVacuumLock 46 NotifyQueueTailLock 47 WaitEventExtensionLock 48 +InjectionPointLock 49 diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/= utils/activity/wait_event_names.txt index d7995931bd..5631d29138 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -319,6 +319,7 @@ XactTruncation "Waiting to execute pg_xact_st= atus or update WrapLimitsVacuum "Waiting to update limits on transaction id and multixact= consumption." NotifyQueueTail "Waiting to update limit on NOTIFY mess= age storage." WaitEventExtension "Waiting to read or update custom wait events informati= on for extensions." +InjectionPoint "Waiting to read or update information related to injection= points." =20 XactBuffer "Waiting for I/O on a transaction status SLRU buffer." CommitTsBuffer "Waiting for I/O on a commit timestamp SLRU buffer." diff --git a/src/backend/utils/misc/Makefile b/src/backend/utils/misc/Makef= ile index c2971c7678..d9f59785b9 100644 --- a/src/backend/utils/misc/Makefile +++ b/src/backend/utils/misc/Makefile @@ -21,6 +21,7 @@ OBJS =3D \ guc_funcs.o \ guc_tables.o \ help_config.o \ + injection_point.o \ pg_config.o \ pg_controldata.o \ pg_rusage.o \ diff --git a/src/backend/utils/misc/injection_point.c b/src/backend/utils/m= isc/injection_point.c new file mode 100644 index 0000000000..6bc349e7c7 --- /dev/null +++ b/src/backend/utils/misc/injection_point.c @@ -0,0 +1,317 @@ +/*------------------------------------------------------------------------- + * + * injection_point.c + * Routines to control and run injection points in the code. + * + * Injection points can be used to call arbitrary callbacks in specific + * places of the code, registering callbacks that would be run in the code + * paths where a named injection point exists. + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/utils/misc/injection_point.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "fmgr.h" +#include "miscadmin.h" +#include "port/pg_bitutils.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" + +#ifdef USE_INJECTION_POINTS + +/* + * Hash table for storing injection points. + * + * InjectionPointHash is used to find an injection point by name. + */ +static HTAB *InjectionPointHash; /* find points from names */ + +/* Field sizes */ +#define INJ_NAME_MAXLEN 64 +#define INJ_LIB_MAXLEN 128 +#define INJ_FUNC_MAXLEN 128 + +typedef struct InjectionPointEntry +{ + char name[INJ_NAME_MAXLEN]; /* hash key */ + char library[INJ_LIB_MAXLEN]; /* library */ + char function[INJ_FUNC_MAXLEN]; /* function */ +} InjectionPointEntry; + +#define INJECTION_POINT_HASH_INIT_SIZE 16 +#define INJECTION_POINT_HASH_MAX_SIZE 128 + +/* + * Local cache of injection callbacks already loaded, stored in + * TopMemoryContext. + */ +typedef struct InjectionPointArrayEntry +{ + char name[INJ_NAME_MAXLEN]; + InjectionPointCallback callback; +} InjectionPointCacheEntry; + +static HTAB *InjectionPointCache =3D NULL; + +/* utilities to handle the local array cache */ +static void +injection_point_cache_add(const char *name, + InjectionPointCallback callback) +{ + InjectionPointCacheEntry *entry; + bool found; + + /* If first time, initialize */ + if (InjectionPointCache =3D=3D NULL) + { + HASHCTL hash_ctl; + + hash_ctl.keysize =3D sizeof(char[INJ_NAME_MAXLEN]); + hash_ctl.entrysize =3D sizeof(InjectionPointCacheEntry); + hash_ctl.hcxt =3D TopMemoryContext; + + InjectionPointCache =3D hash_create("InjectionPoint cache hash", + INJECTION_POINT_HASH_MAX_SIZE, + &hash_ctl, + HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); + } + + entry =3D (InjectionPointCacheEntry *) + hash_search(InjectionPointCache, name, HASH_ENTER, &found); + + if (!found) + { + memcpy(entry->name, name, strlen(name)); + entry->callback =3D callback; + } +} + +/* + * Remove entry from the local cache. Note that this leaks a callback + * loaded but removed later on, which should have no consequence from + * a testing perspective. + */ +static void +injection_point_cache_remove(const char *name) +{ + /* Leave if no cache */ + if (InjectionPointCache =3D=3D NULL) + return; + + (void) hash_search(InjectionPointCache, name, HASH_REMOVE, NULL); +} + +static InjectionPointCallback +injection_point_cache_get(const char *name) +{ + bool found; + InjectionPointCacheEntry *entry; + + /* no callback if no cache yet */ + if (InjectionPointCache =3D=3D NULL) + return NULL; + + entry =3D (InjectionPointCacheEntry *) + hash_search(InjectionPointCache, name, HASH_FIND, &found); + + if (found) + return entry->callback; + + return NULL; +} +#endif /* USE_INJECTION_POINTS */ + +/* + * Return the space for dynamic shared hash table. + */ +Size +InjectionPointShmemSize(void) +{ +#ifdef USE_INJECTION_POINTS + Size sz =3D 0; + + sz =3D add_size(sz, hash_estimate_size(INJECTION_POINT_HASH_MAX_SIZE, + sizeof(InjectionPointEntry))); + return sz; +#else + return 0; +#endif +} + +/* + * Allocate shmem space for dynamic shared hash. + */ +void +InjectionPointShmemInit(void) +{ +#ifdef USE_INJECTION_POINTS + HASHCTL info; + + /* key is a NULL-terminated string */ + info.keysize =3D sizeof(char[INJ_NAME_MAXLEN]); + info.entrysize =3D sizeof(InjectionPointEntry); + InjectionPointHash =3D ShmemInitHash("InjectionPoint hash", + INJECTION_POINT_HASH_INIT_SIZE, + INJECTION_POINT_HASH_MAX_SIZE, + &info, + HASH_ELEM | HASH_STRINGS); +#endif +} + +#ifdef USE_INJECTION_POINTS +static bool +file_exists(const char *name) +{ + struct stat st; + + Assert(name !=3D NULL); + if (stat(name, &st) =3D=3D 0) + return !S_ISDIR(st.st_mode); + else if (!(errno =3D=3D ENOENT || errno =3D=3D ENOTDIR)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", name))); + return false; +} +#endif + +/* + * Attach a new injection point. + */ +void +InjectionPointAttach(const char *name, + const char *library, + const char *function) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointEntry *entry_by_name; + bool found; + + if (strlen(name) >=3D INJ_NAME_MAXLEN) + elog(ERROR, "injection point name %s too long", name); + if (strlen(library) >=3D INJ_LIB_MAXLEN) + elog(ERROR, "injection point library %s too long", library); + if (strlen(function) >=3D INJ_FUNC_MAXLEN) + elog(ERROR, "injection point function %s too long", function); + + /* + * Allocate and register a new injection point. A new point should not + * exist. For testing purposes this should be fine. + */ + LWLockAcquire(InjectionPointLock, LW_EXCLUSIVE); + entry_by_name =3D (InjectionPointEntry *) + hash_search(InjectionPointHash, name, + HASH_ENTER, &found); + if (found) + { + LWLockRelease(InjectionPointLock); + elog(ERROR, "injection point \"%s\" already defined", name); + } + + /* Save the entry */ + memcpy(entry_by_name->name, name, sizeof(entry_by_name->name)); + entry_by_name->name[INJ_NAME_MAXLEN - 1] =3D '\0'; + memcpy(entry_by_name->library, library, sizeof(entry_by_name->library)); + entry_by_name->library[INJ_LIB_MAXLEN - 1] =3D '\0'; + memcpy(entry_by_name->function, function, sizeof(entry_by_name->function)= ); + entry_by_name->function[INJ_FUNC_MAXLEN - 1] =3D '\0'; + + LWLockRelease(InjectionPointLock); + +#else + elog(ERROR, "Injection points are not supported by this build"); +#endif +} + +/* + * Detach an existing injection point. + */ +void +InjectionPointDetach(const char *name) +{ +#ifdef USE_INJECTION_POINTS + bool found; + + LWLockAcquire(InjectionPointLock, LW_EXCLUSIVE); + hash_search(InjectionPointHash, name, HASH_REMOVE, &found); + LWLockRelease(InjectionPointLock); + + if (!found) + elog(ERROR, "injection point \"%s\" not found", name); + +#else + elog(ERROR, "Injection points are not supported by this build"); +#endif +} + +/* + * Execute an injection point, if defined. + * + * Check first the shared hash table, and adapt the local cache + * depending on that as it could be possible that an entry to run + * has been removed. + */ +void +InjectionPointRun(const char *name) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointEntry *entry_by_name; + bool found; + InjectionPointCallback injection_callback; + + LWLockAcquire(InjectionPointLock, LW_SHARED); + entry_by_name =3D (InjectionPointEntry *) + hash_search(InjectionPointHash, name, + HASH_FIND, &found); + LWLockRelease(InjectionPointLock); + + /* + * If not found, do nothing and remove it from the local cache if it + * existed there. + */ + if (!found) + { + injection_point_cache_remove(name); + return; + } + + /* + * Check if the callback exists in the local cache, to avoid unnecessary + * external loads. + */ + injection_callback =3D injection_point_cache_get(name); + if (injection_callback =3D=3D NULL) + { + char path[MAXPGPATH]; + + /* Found, so just run the callback registered */ + snprintf(path, MAXPGPATH, "%s/%s%s", pkglib_path, + entry_by_name->library, DLSUFFIX); + + if (!file_exists(path)) + elog(ERROR, "could not find injection library \"%s\"", path); + + injection_callback =3D (InjectionPointCallback) + load_external_function(path, entry_by_name->function, true, NULL); + + /* add it to the local cache when found */ + injection_point_cache_add(name, injection_callback); + } + + injection_callback(name); +#else + elog(ERROR, "Injection points are not supported by this build"); +#endif +} diff --git a/src/backend/utils/misc/meson.build b/src/backend/utils/misc/me= son.build index f719c97c05..1438859b69 100644 --- a/src/backend/utils/misc/meson.build +++ b/src/backend/utils/misc/meson.build @@ -6,6 +6,7 @@ backend_sources +=3D files( 'guc_funcs.c', 'guc_tables.c', 'help_config.c', + 'injection_point.c', 'pg_config.c', 'pg_controldata.c', 'pg_rusage.c', diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml index b23b35cd8e..d5a2fcb084 100644 --- a/doc/src/sgml/installation.sgml +++ b/doc/src/sgml/installation.sgml @@ -1676,6 +1676,21 @@ build-postgresql: =20 + + + + + Compiles PostgreSQL with support for + injection points in the server. This is valuable to inject + user-defined code to force specific conditions to happen on the + server in pre-defined code paths. This option is disabled by defa= ult. + See for more deta= ils. + This option is only for developers to test specific concurrency + scenarios. + + + + @@ -3184,6 +3199,21 @@ ninja install =20 + + + + + Compiles PostgreSQL with support for + injection points in the server. This is valuable to inject + user-defined code to force specific conditions to happen on the + server in pre-defined code paths. This option is disabled by defa= ult. + See for more deta= ils. + This option is only for developers to test specific concurrency + scenarios. + + + + diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index 89116ae74c..66cc94b03b 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -3510,6 +3510,62 @@ uint32 WaitEventExtensionNew(const char *wait_event_= name) =20 + + Injection Points + + + Add-ins can define injection points, that can register callbacks + to run user-defined code when going through a specific code path, + by calling: + +extern void InjectionPointAttach(const char *name, + const char *library, + const char *function); + + + name is the name of the injection point, that + will execute the function loaded from + library. + Injection points are saved in a hash table in shared memory, and + last until the server is shut down. + + + + Here is an example of callback for + InjectionPointCallback: + +static void +custom_injection_callback(const char *name) +{ + elog(NOTICE, "%s: executed custom callback", name); +} + + + + + Once an injection point is defined, running it requires to use + the following macro to trigger the callback given in a wanted code + path: + +INJECTION_POINT(name); + + + + + Optionally, it is possible to detach injection points by calling: + +extern void InjectionPointDetach(const char *name); + + + + + Enabling injections points requires + from + configure or + + Using C++ for Extensibility =20 diff --git a/configure b/configure index 217704e9ca..fbac8dd23f 100755 --- a/configure +++ b/configure @@ -759,6 +759,7 @@ CPPFLAGS LDFLAGS CFLAGS CC +enable_injection_points enable_tap_tests enable_dtrace DTRACEFLAGS @@ -839,6 +840,7 @@ enable_profiling enable_coverage enable_dtrace enable_tap_tests +enable_injection_points with_blocksize with_segsize with_segsize_blocks @@ -1532,6 +1534,8 @@ Optional Features: --enable-coverage build with coverage testing instrumentation --enable-dtrace build with DTrace support --enable-tap-tests enable TAP tests (requires Perl and IPC::Run) + --enable-injection-points + enable injection points (for testing) --enable-depend turn on automatic dependency tracking --enable-cassert enable assertion checks (for debugging) --disable-largefile omit support for large files @@ -3682,6 +3686,36 @@ fi =20 =20 =20 +# +# Injection points +# + + +# Check whether --enable-injection-points was given. +if test "${enable_injection_points+set}" =3D set; then : + enableval=3D$enable_injection_points; + case $enableval in + yes) + +$as_echo "#define USE_INJECTION_POINTS 1" >>confdefs.h + + ;; + no) + : + ;; + *) + as_fn_error $? "no argument expected for --enable-injection-points o= ption" "$LINENO" 5 + ;; + esac + +else + enable_injection_points=3Dno + +fi + + + + # # Block size # diff --git a/configure.ac b/configure.ac index e49de9e4f0..1507de55b7 100644 --- a/configure.ac +++ b/configure.ac @@ -250,6 +250,13 @@ PGAC_ARG_BOOL(enable, tap-tests, no, [enable TAP tests (requires Perl and IPC::Run)]) AC_SUBST(enable_tap_tests) =20 +# +# Injection points +# +PGAC_ARG_BOOL(enable, injection-points, no, [enable injection points (for = testing)], + [AC_DEFINE([USE_INJECTION_POINTS], 1, [Define to 1 to build = with injection points. (--enable-injection-points)])]) +AC_SUBST(enable_injection_points) + # # Block size # diff --git a/meson.build b/meson.build index 52c2a37c41..cae865f640 100644 --- a/meson.build +++ b/meson.build @@ -431,6 +431,7 @@ meson_bin =3D find_program(meson_binpath, native: true) ############################################################### =20 cdata.set('USE_ASSERT_CHECKING', get_option('cassert') ? 1 : false) +cdata.set('USE_INJECTION_POINTS', get_option('injection_points') ? 1 : fal= se) =20 blocksize =3D get_option('blocksize').to_int() * 1024 =20 diff --git a/meson_options.txt b/meson_options.txt index be1b327f54..7a5102df1a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -43,6 +43,9 @@ option('cassert', type: 'boolean', value: false, option('tap_tests', type: 'feature', value: 'auto', description: 'Enable TAP tests') =20 +option('injection_points', type: 'boolean', value: false, + description: 'Enable injection points') + option('PG_TEST_EXTRA', type: 'string', value: '', description: 'Enable selected extra tests') =20 diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 104e5de0fe..7c7fd77b01 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -203,6 +203,7 @@ enable_nls =3D @enable_nls@ enable_debug =3D @enable_debug@ enable_dtrace =3D @enable_dtrace@ enable_coverage =3D @enable_coverage@ +enable_injection_points =3D @enable_injection_points@ enable_tap_tests =3D @enable_tap_tests@ =20 python_includespec =3D @python_includespec@ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs= =2Elist index ba41149b88..4e495d45fa 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1145,6 +1145,8 @@ IdentLine IdentifierLookup IdentifySystemCmd IfStackElem +InjectionPointCacheEntry +InjectionPointEntry ImportForeignSchemaStmt ImportForeignSchemaType ImportForeignSchema_function --=20 2.43.0 --GwmmnaKI4J/fD1SE Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="v6-0002-Add-test-module-test_injection_points.patch" Content-Transfer-Encoding: quoted-printable =46rom 1d1d27e9b696a01d3468acf51d13aca92b629e59 Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Tue, 12 Dec 2023 11:40:50 +0100 Subject: [PATCH v6 2/4] Add test module test_injection_points This is a test facility aimed at providing basic coverage for the code routines of injection points. This will be extended with more tests. --- src/test/modules/Makefile | 7 ++ src/test/modules/meson.build | 1 + .../modules/test_injection_points/.gitignore | 4 + .../modules/test_injection_points/Makefile | 22 ++++ .../expected/test_injection_points.out | 117 ++++++++++++++++++ .../modules/test_injection_points/meson.build | 37 ++++++ .../sql/test_injection_points.sql | 33 +++++ .../test_injection_points--1.0.sql | 36 ++++++ .../test_injection_points.c | 91 ++++++++++++++ .../test_injection_points.control | 4 + 10 files changed, 352 insertions(+) create mode 100644 src/test/modules/test_injection_points/.gitignore create mode 100644 src/test/modules/test_injection_points/Makefile create mode 100644 src/test/modules/test_injection_points/expected/test_in= jection_points.out create mode 100644 src/test/modules/test_injection_points/meson.build create mode 100644 src/test/modules/test_injection_points/sql/test_injecti= on_points.sql create mode 100644 src/test/modules/test_injection_points/test_injection_p= oints--1.0.sql create mode 100644 src/test/modules/test_injection_points/test_injection_p= oints.c create mode 100644 src/test/modules/test_injection_points/test_injection_p= oints.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 5d33fa6a9a..9a55a6924e 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -37,6 +37,13 @@ SUBDIRS =3D \ worker_spi \ xid_wraparound =20 + +ifeq ($(enable_injection_points),yes) +SUBDIRS +=3D test_injection_points +else +ALWAYS_SUBDIRS +=3D test_injection_points +endif + ifeq ($(with_ssl),openssl) SUBDIRS +=3D ssl_passphrase_callback else diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index b76f588559..dc0048a6e4 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -17,6 +17,7 @@ subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_extensions') subdir('test_ginpostinglist') +subdir('test_injection_points') subdir('test_integerset') subdir('test_lfind') subdir('test_misc') diff --git a/src/test/modules/test_injection_points/.gitignore b/src/test/m= odules/test_injection_points/.gitignore new file mode 100644 index 0000000000..5dcb3ff972 --- /dev/null +++ b/src/test/modules/test_injection_points/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_injection_points/Makefile b/src/test/mod= ules/test_injection_points/Makefile new file mode 100644 index 0000000000..65bcdde782 --- /dev/null +++ b/src/test/modules/test_injection_points/Makefile @@ -0,0 +1,22 @@ +# src/test/modules/test_injection_points/Makefile + +MODULE_big =3D test_injection_points +OBJS =3D \ + $(WIN32RES) \ + test_injection_points.o +PGFILEDESC =3D "test_injection_points - test injection points" + +EXTENSION =3D test_injection_points +DATA =3D test_injection_points--1.0.sql +REGRESS =3D test_injection_points + +ifdef USE_PGXS +PG_CONFIG =3D pg_config +PGXS :=3D $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir =3D src/test/modules/test_injection_points +top_builddir =3D ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_injection_points/expected/test_injection= _points.out b/src/test/modules/test_injection_points/expected/test_injectio= n_points.out new file mode 100644 index 0000000000..a8ddae0aad --- /dev/null +++ b/src/test/modules/test_injection_points/expected/test_injection_points= =2Eout @@ -0,0 +1,117 @@ +CREATE EXTENSION test_injection_points; +SELECT test_injection_points_attach('TestInjectionBooh', 'booh'); +ERROR: incorrect mode "booh" for injection point creation +SELECT test_injection_points_attach('TestInjectionError', 'error'); + test_injection_points_attach=20 +------------------------------ +=20 +(1 row) + +SELECT test_injection_points_attach('TestInjectionLog', 'notice'); + test_injection_points_attach=20 +------------------------------ +=20 +(1 row) + +SELECT test_injection_points_attach('TestInjectionLog2', 'notice'); + test_injection_points_attach=20 +------------------------------ +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionBooh'); -- nothing + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionLog2'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog2 + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionLog'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionError'); -- error +ERROR: error triggered for injection point TestInjectionError +-- Re-load and run again. +\c +SELECT test_injection_points_run('TestInjectionLog2'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog2 + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionLog'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionError'); -- error +ERROR: error triggered for injection point TestInjectionError +-- Remove one entry and check the other one. +SELECT test_injection_points_detach('TestInjectionError'); -- ok + test_injection_points_detach=20 +------------------------------ +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionLog'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionError'); -- nothing + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +-- All entries removed, nothing happens +SELECT test_injection_points_detach('TestInjectionLog'); -- ok + test_injection_points_detach=20 +------------------------------ +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionLog'); -- nothing + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionError'); -- nothing + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_run('TestInjectionLog2'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog2 + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +SELECT test_injection_points_detach('TestInjectionLog'); -- fails +ERROR: injection point "TestInjectionLog" not found +SELECT test_injection_points_run('TestInjectionLog2'); -- notice +NOTICE: notice triggered for injection point TestInjectionLog2 + test_injection_points_run=20 +--------------------------- +=20 +(1 row) + +DROP EXTENSION test_injection_points; diff --git a/src/test/modules/test_injection_points/meson.build b/src/test/= modules/test_injection_points/meson.build new file mode 100644 index 0000000000..7509a102ef --- /dev/null +++ b/src/test/modules/test_injection_points/meson.build @@ -0,0 +1,37 @@ +# Copyright (c) 2022-2023, PostgreSQL Global Development Group + +if not get_option('injection_points') + subdir_done() +endif + +test_injection_points_sources =3D files( + 'test_injection_points.c', +) + +if host_system =3D=3D 'windows' + test_injection_points_sources +=3D rc_lib_gen.process(win32ver_rc, extra= _args: [ + '--NAME', 'test_injection_points', + '--FILEDESC', 'test_injection_points - test injection points',]) +endif + +test_injection_points =3D shared_module('test_injection_points', + test_injection_points_sources, + kwargs: pg_test_mod_args, +) +test_install_libs +=3D test_injection_points + +test_install_data +=3D files( + 'test_injection_points.control', + 'test_injection_points--1.0.sql', +) + +tests +=3D { + 'name': 'test_injection_points', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_injection_points', + ], + }, +} diff --git a/src/test/modules/test_injection_points/sql/test_injection_poin= ts.sql b/src/test/modules/test_injection_points/sql/test_injection_points.s= ql new file mode 100644 index 0000000000..8f23f4c044 --- /dev/null +++ b/src/test/modules/test_injection_points/sql/test_injection_points.sql @@ -0,0 +1,33 @@ +CREATE EXTENSION test_injection_points; + +SELECT test_injection_points_attach('TestInjectionBooh', 'booh'); +SELECT test_injection_points_attach('TestInjectionError', 'error'); +SELECT test_injection_points_attach('TestInjectionLog', 'notice'); +SELECT test_injection_points_attach('TestInjectionLog2', 'notice'); + +SELECT test_injection_points_run('TestInjectionBooh'); -- nothing +SELECT test_injection_points_run('TestInjectionLog2'); -- notice +SELECT test_injection_points_run('TestInjectionLog'); -- notice +SELECT test_injection_points_run('TestInjectionError'); -- error + +-- Re-load and run again. +\c +SELECT test_injection_points_run('TestInjectionLog2'); -- notice +SELECT test_injection_points_run('TestInjectionLog'); -- notice +SELECT test_injection_points_run('TestInjectionError'); -- error + +-- Remove one entry and check the other one. +SELECT test_injection_points_detach('TestInjectionError'); -- ok +SELECT test_injection_points_run('TestInjectionLog'); -- notice +SELECT test_injection_points_run('TestInjectionError'); -- nothing +-- All entries removed, nothing happens +SELECT test_injection_points_detach('TestInjectionLog'); -- ok +SELECT test_injection_points_run('TestInjectionLog'); -- nothing +SELECT test_injection_points_run('TestInjectionError'); -- nothing +SELECT test_injection_points_run('TestInjectionLog2'); -- notice + +SELECT test_injection_points_detach('TestInjectionLog'); -- fails + +SELECT test_injection_points_run('TestInjectionLog2'); -- notice + +DROP EXTENSION test_injection_points; diff --git a/src/test/modules/test_injection_points/test_injection_points--= 1.0.sql b/src/test/modules/test_injection_points/test_injection_points--1.0= =2Esql new file mode 100644 index 0000000000..1c0a689ae2 --- /dev/null +++ b/src/test/modules/test_injection_points/test_injection_points--1.0.sql @@ -0,0 +1,36 @@ +/* src/test/modules/test_injection_points/test_injection_points--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_injection_points" to load this file. \quit + +-- +-- test_injection_points_attach() +-- +-- Attaches an injection point using callbacks from one of the predefined +-- modes. +-- +CREATE FUNCTION test_injection_points_attach(IN point_name TEXT, + IN mode text) +RETURNS void +AS 'MODULE_PATHNAME', 'test_injection_points_attach' +LANGUAGE C STRICT PARALLEL UNSAFE; + +-- +-- test_injection_points_run() +-- +-- Executes an injection point. +-- +CREATE FUNCTION test_injection_points_run(IN point_name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_injection_points_run' +LANGUAGE C STRICT PARALLEL UNSAFE; + +-- +-- test_injection_points_detach() +-- +-- Detaches an injection point. +-- +CREATE FUNCTION test_injection_points_detach(IN point_name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_injection_points_detach' +LANGUAGE C STRICT PARALLEL UNSAFE; diff --git a/src/test/modules/test_injection_points/test_injection_points.c= b/src/test/modules/test_injection_points/test_injection_points.c new file mode 100644 index 0000000000..efb2c74c47 --- /dev/null +++ b/src/test/modules/test_injection_points/test_injection_points.c @@ -0,0 +1,91 @@ +/*------------------------------------------------------------------------= -- + * + * test_injection_points.c + * Code for testing injection points. + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/test/modules/test_injection_points/test_injection_points.c + * + * Injection points are able to trigger user-defined callbacks in pre-defi= ned + * code paths. + * + * -----------------------------------------------------------------------= -- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "utils/builtins.h" +#include "utils/injection_point.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void test_injection_error(const char *name); +extern PGDLLEXPORT void test_injection_notice(const char *name); + +/* Set of callbacks available at point creation */ +void +test_injection_error(const char *name) +{ + elog(ERROR, "error triggered for injection point %s", name); +} + +void +test_injection_notice(const char *name) +{ + elog(NOTICE, "notice triggered for injection point %s", name); +} + +/* + * SQL function for creating an injection point. + */ +PG_FUNCTION_INFO_V1(test_injection_points_attach); +Datum +test_injection_points_attach(PG_FUNCTION_ARGS) +{ + char *name =3D text_to_cstring(PG_GETARG_TEXT_PP(0)); + char *mode =3D text_to_cstring(PG_GETARG_TEXT_PP(1)); + char *function; + + if (strcmp(mode, "error") =3D=3D 0) + function =3D "test_injection_error"; + else if (strcmp(mode, "notice") =3D=3D 0) + function =3D "test_injection_notice"; + else + elog(ERROR, "incorrect mode \"%s\" for injection point creation", mode); + + InjectionPointAttach(name, "test_injection_points", function); + + PG_RETURN_VOID(); +} + +/* + * SQL function for triggering an injection point. + */ +PG_FUNCTION_INFO_V1(test_injection_points_run); +Datum +test_injection_points_run(PG_FUNCTION_ARGS) +{ + char *name =3D text_to_cstring(PG_GETARG_TEXT_PP(0)); + + INJECTION_POINT(name); + + PG_RETURN_VOID(); +} + +/* + * SQL function for dropping an injection point. + */ +PG_FUNCTION_INFO_V1(test_injection_points_detach); +Datum +test_injection_points_detach(PG_FUNCTION_ARGS) +{ + char *name =3D text_to_cstring(PG_GETARG_TEXT_PP(0)); + + InjectionPointDetach(name); + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_injection_points/test_injection_points.c= ontrol b/src/test/modules/test_injection_points/test_injection_points.contr= ol new file mode 100644 index 0000000000..a13657cfc6 --- /dev/null +++ b/src/test/modules/test_injection_points/test_injection_points.control @@ -0,0 +1,4 @@ +comment =3D 'Test code for injection points' +default_version =3D '1.0' +module_pathname =3D '$libdir/test_injection_points' +relocatable =3D true --=20 2.43.0 --GwmmnaKI4J/fD1SE Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="v6-0003-Add-regression-test-to-show-snapbuild-consistency.patch" Content-Transfer-Encoding: quoted-printable =46rom aaf4b21dffcabd55f7dd5fb9d1f35f13ba434153 Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Thu, 16 Nov 2023 14:28:22 +0900 Subject: [PATCH v6 3/4] Add regression test to show snapbuild consistency Reverting 409f9ca44713 causes the test to fail. The test added here relies on the existing callbacks in test_injection_points. --- src/backend/replication/logical/snapbuild.c | 3 ++ .../modules/test_injection_points/Makefile | 2 + .../modules/test_injection_points/meson.build | 5 ++ .../t/001_snapshot_status.pl | 47 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 src/test/modules/test_injection_points/t/001_snapshot_s= tatus.pl diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/repl= ication/logical/snapbuild.c index fec190a8b2..3491e5a872 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -141,6 +141,7 @@ #include "storage/procarray.h" #include "storage/standby.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/snapshot.h" @@ -654,6 +655,8 @@ SnapBuildInitialSnapshot(SnapBuild *builder) snap->xcnt =3D newxcnt; snap->xip =3D newxip; =20 + INJECTION_POINT("SnapBuildInitialSnapshot"); + return snap; } =20 diff --git a/src/test/modules/test_injection_points/Makefile b/src/test/mod= ules/test_injection_points/Makefile index 65bcdde782..4696c1b013 100644 --- a/src/test/modules/test_injection_points/Makefile +++ b/src/test/modules/test_injection_points/Makefile @@ -10,6 +10,8 @@ EXTENSION =3D test_injection_points DATA =3D test_injection_points--1.0.sql REGRESS =3D test_injection_points =20 +TAP_TESTS =3D 1 + ifdef USE_PGXS PG_CONFIG =3D pg_config PGXS :=3D $(shell $(PG_CONFIG) --pgxs) diff --git a/src/test/modules/test_injection_points/meson.build b/src/test/= modules/test_injection_points/meson.build index 7509a102ef..6006b38f3d 100644 --- a/src/test/modules/test_injection_points/meson.build +++ b/src/test/modules/test_injection_points/meson.build @@ -34,4 +34,9 @@ tests +=3D { 'test_injection_points', ], }, + 'tap': { + 'tests': [ + 't/001_snapshot_status.pl', + ], + } } diff --git a/src/test/modules/test_injection_points/t/001_snapshot_status.p= l b/src/test/modules/test_injection_points/t/001_snapshot_status.pl new file mode 100644 index 0000000000..ca5c6cc7a4 --- /dev/null +++ b/src/test/modules/test_injection_points/t/001_snapshot_status.pl @@ -0,0 +1,47 @@ +# Test consistent of initial snapshot data. + +# This requires a node with wal_level=3Dlogical combined with an injection +# point that forces a failure when a snapshot is initially built with a +# logical slot created. +# +# See bug https://postgr.es/m/CAFiTN-s0zA1Kj0ozGHwkYkHwa5U0zUE94RSc_g81Wrp= cETB5=3Dw@mail.gmail.com. + +use strict; +use warnings; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node =3D PostgreSQL::Test::Cluster->new('node'); +$node->init(allows_streaming =3D> 'logical'); +$node->start; + +$node->safe_psql('postgres', 'CREATE EXTENSION test_injection_points;'); +$node->safe_psql('postgres', + "SELECT test_injection_points_attach('SnapBuildInitialSnapshot', 'error'= );"); + +my $node_host =3D $node->host; +my $node_port =3D $node->port; +my $connstr_common =3D "host=3D$node_host port=3D$node_port"; +my $connstr_db =3D "$connstr_common replication=3Ddatabase dbname=3Dpostgr= es"; + +# This requires a single session, with two commands. +my $psql_session =3D + $node->background_psql('postgres', on_error_stop =3D> 0, + extra_params =3D> [ '-d', $connstr_db ]); +my ($output, $ret) =3D $psql_session->query( + 'CREATE_REPLICATION_SLOT "slot" LOGICAL "pgoutput";'); +ok($ret !=3D 0, "First CREATE_REPLICATION_SLOT fails on injected error"); + +# Now remove the injected error and check that the second command works. +$node->safe_psql('postgres', + "SELECT test_injection_points_detach('SnapBuildInitialSnapshot');"); + +($output, $ret) =3D $psql_session->query( + 'CREATE_REPLICATION_SLOT "slot" LOGICAL "pgoutput";'); +print "BOO" . substr($output, 0, 4) . "\n"; +ok(substr($output, 0, 4) eq 'slot', + "Second CREATE_REPLICATION_SLOT passes"); + +done_testing(); --=20 2.43.0 --GwmmnaKI4J/fD1SE Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="v6-0004-Add-basic-test-for-promotion-and-restart-race-con.patch" Content-Transfer-Encoding: quoted-printable =46rom f184ace1b21c70c09b41bc54ebeb5f341098a96c Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Thu, 16 Nov 2023 14:42:31 +0900 Subject: [PATCH v6 4/4] Add basic test for promotion and restart race condition This test fails after 7863ee4def65 is reverted. test_injection_points is extended so as it is possible to add condition variables to wait for in the point callbacks, with a SQL function to broadcast condition variables that may be sleeping. I guess that this should be extended so as there is more than one condition variable stored in shmem for this module, controlling which variable to wait for directly in the callback itself, but that's not really necessary now. --- src/backend/access/transam/xlog.c | 7 + .../modules/test_injection_points/meson.build | 1 + .../t/002_invalid_checkpoint_after_promote.pl | 132 ++++++++++++++++++ .../test_injection_points--1.0.sql | 10 ++ .../test_injection_points.c | 73 ++++++++++ 5 files changed, 223 insertions(+) create mode 100644 src/test/modules/test_injection_points/t/002_invalid_ch= eckpoint_after_promote.pl diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam= /xlog.c index 01e0484584..ece31bb2a6 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -99,6 +99,7 @@ #include "storage/sync.h" #include "utils/guc_hooks.h" #include "utils/guc_tables.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/relmapper.h" @@ -7345,6 +7346,12 @@ CreateRestartPoint(int flags) =20 CheckPointGuts(lastCheckPoint.redo, flags); =20 + /* + * This location is important to be after CheckPointGuts() to ensure + * that some work has happened. + */ + INJECTION_POINT("CreateRestartPoint"); + /* * Remember the prior checkpoint's redo ptr for * UpdateCheckPointDistanceEstimate() diff --git a/src/test/modules/test_injection_points/meson.build b/src/test/= modules/test_injection_points/meson.build index 6006b38f3d..6ebdc728b7 100644 --- a/src/test/modules/test_injection_points/meson.build +++ b/src/test/modules/test_injection_points/meson.build @@ -37,6 +37,7 @@ tests +=3D { 'tap': { 'tests': [ 't/001_snapshot_status.pl', + 't/002_invalid_checkpoint_after_promote.pl', ], } } diff --git a/src/test/modules/test_injection_points/t/002_invalid_checkpoin= t_after_promote.pl b/src/test/modules/test_injection_points/t/002_invalid_c= heckpoint_after_promote.pl new file mode 100644 index 0000000000..2da243e871 --- /dev/null +++ b/src/test/modules/test_injection_points/t/002_invalid_checkpoint_after= _promote.pl @@ -0,0 +1,132 @@ +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Time::HiRes qw(usleep nanosleep); +use Test::More; + +# initialize primary node +my $node_primary =3D PostgreSQL::Test::Cluster->new('master'); +$node_primary->init(allows_streaming =3D> 1); +$node_primary->append_conf( + 'postgresql.conf', q[ +checkpoint_timeout =3D 30s +log_checkpoints =3D on +restart_after_crash =3D on +]); +$node_primary->start; +my $backup_name =3D 'my_backup'; +$node_primary->backup($backup_name); + +# setup a standby +my $node_standby =3D PostgreSQL::Test::Cluster->new('standby1'); +$node_standby->init_from_backup($node_primary, $backup_name, has_streaming= =3D> 1); +$node_standby->start; + +# dummy table for the upcoming tests. +$node_primary->safe_psql('postgres', 'checkpoint'); +$node_primary->safe_psql('postgres', 'CREATE TABLE prim_tab (a int);'); + +# Register a injection point on the standby so as the follow-up +# restart point running on it will wait. +$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_injection_poin= ts;'); +# Wait until the extension has been created on the standby +$node_primary->wait_for_replay_catchup($node_standby); +# This causes a restartpoint to wait on a standby. +$node_standby->safe_psql('postgres', + "SELECT test_injection_points_attach('CreateRestartPoint', 'wait');"); + +# Execute a restart point on the standby, that will be waited on. +# This needs to be in the background as we'll wait on it. +my $logstart =3D -s $node_standby->logfile; +my $psql_session =3D + $node_standby->background_psql('postgres', on_error_stop =3D> 0); +$psql_session->query_until(qr/starting_checkpoint/, q( + \echo starting_checkpoint + CHECKPOINT; +)); + +# Switch one WAL segment to make the restartpoint remove it. +$node_primary->safe_psql('postgres', 'INSERT INTO prim_tab VALUES (1);'); +$node_primary->safe_psql('postgres', 'SELECT pg_switch_wal();'); +$node_primary->wait_for_replay_catchup($node_standby); + +# Wait until the checkpointer is in the middle of the restartpoint +# processing. +ok( $node_standby->poll_query_until( + 'postgres', + qq[SELECT count(*) FROM pg_stat_activity + WHERE backend_type =3D 'checkpointer' AND wait_event =3D 'test_= injection_wait' ;], + '1'), + 'checkpointer is waiting at restart point' + ) or die "Timed out while waiting for checkpointer to run restartpoint= "; + + +# Restartpoint should have started on standby. +my $log =3D slurp_file($node_standby->logfile, $logstart); +my $checkpoint_start =3D 0; +if ($log =3D~ m/restartpoint starting: immediate wait/) +{ + $checkpoint_start =3D 1; +} +is($checkpoint_start, 1, 'restartpoint has started'); + +# promote during restartpoint +$node_primary->stop; +$node_standby->promote; + +# Update the start position before waking up the checkpointer! +$logstart =3D -s $node_standby->logfile; + +# Now wake up the checkpointer +$node_standby->safe_psql('postgres', + "SELECT test_injection_points_wake();"); + +# wait until checkpoint completes on the newly-promoted standby. +my $checkpoint_complete =3D 0; +for (my $i =3D 0; $i < 3000; $i++) +{ + my $log =3D slurp_file($node_standby->logfile, $logstart); + if ($log =3D~ m/restartpoint complete/) + { + $checkpoint_complete =3D 1; + last; + } + usleep(100_000); +} +is($checkpoint_complete, 1, 'restartpoint has completed'); + +# kill SIGKILL a backend, and all backend will restart. Note that previous= checkpoint has not completed. +my $psql_timeout =3D IPC::Run::timer(3600); +my ($killme_stdin, $killme_stdout, $killme_stderr) =3D ('', '', ''); +my $killme =3D IPC::Run::start( + [ 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=3D1', '-f', '-', '-d', $node_stan= dby->connstr('postgres') ], + '<', + \$killme_stdin, + '>', + \$killme_stdout, + '2>', + \$killme_stderr, + $psql_timeout); +$killme_stdin .=3D q[ +SELECT pg_backend_pid(); +]; +$killme->pump until $killme_stdout =3D~ /[[:digit:]]+[\r\n]$/; +my $pid =3D $killme_stdout; +chomp($pid); +my $ret =3D PostgreSQL::Test::Utils::system_log('pg_ctl', 'kill', 'KILL', = $pid); +is($ret, 0, 'killed process with KILL'); +my $stdout; +my $stderr; + +# after recovery, the server will not start, and log PANIC: could not loca= te a valid checkpoint record +for (my $i =3D 0; $i < 30; $i++) +{ + ($ret, $stdout, $stderr) =3D $node_standby->psql('postgres', 'select 1= '); + last if $ret =3D=3D 0; + sleep(1); +} +is($ret, 0, "psql connect success"); +is($stdout, 1, "psql select 1"); + +done_testing(); diff --git a/src/test/modules/test_injection_points/test_injection_points--= 1.0.sql b/src/test/modules/test_injection_points/test_injection_points--1.0= =2Esql index 1c0a689ae2..05f97f0982 100644 --- a/src/test/modules/test_injection_points/test_injection_points--1.0.sql +++ b/src/test/modules/test_injection_points/test_injection_points--1.0.sql @@ -25,6 +25,16 @@ RETURNS void AS 'MODULE_PATHNAME', 'test_injection_points_run' LANGUAGE C STRICT PARALLEL UNSAFE; =20 +-- +-- test_injection_points_wake() +-- +-- Wakes a condition variable executed in an injection point. +-- +CREATE FUNCTION test_injection_points_wake() +RETURNS void +AS 'MODULE_PATHNAME', 'test_injection_points_wake' +LANGUAGE C STRICT PARALLEL UNSAFE; + -- -- test_injection_points_detach() -- diff --git a/src/test/modules/test_injection_points/test_injection_points.c= b/src/test/modules/test_injection_points/test_injection_points.c index efb2c74c47..8b837d85d3 100644 --- a/src/test/modules/test_injection_points/test_injection_points.c +++ b/src/test/modules/test_injection_points/test_injection_points.c @@ -18,13 +18,56 @@ #include "postgres.h" =20 #include "fmgr.h" +#include "storage/condition_variable.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" #include "utils/builtins.h" #include "utils/injection_point.h" +#include "utils/wait_event.h" =20 PG_MODULE_MAGIC; =20 +/* Shared state information for injection points. */ +typedef struct TestInjectionPointSharedState +{ + /* + * Wait variable that can be registered at a given point, and that can be + * awakened via SQL. + */ + ConditionVariable wait_point; +} TestInjectionPointSharedState; + +/* Pointer to shared-memory state. */ +static TestInjectionPointSharedState *inj_state =3D NULL; + +/* Wait event when waiting on condition variable */ +static uint32 test_injection_wait_event =3D 0; + extern PGDLLEXPORT void test_injection_error(const char *name); extern PGDLLEXPORT void test_injection_notice(const char *name); +extern PGDLLEXPORT void test_injection_wait(const char *name); + + +static void +test_injection_init_shmem(void) +{ + bool found; + + if (inj_state !=3D NULL) + return; + + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + inj_state =3D ShmemInitStruct("test_injection_points", + sizeof(TestInjectionPointSharedState), + &found); + if (!found) + { + /* First time through ... */ + MemSet(inj_state, 0, sizeof(TestInjectionPointSharedState)); + ConditionVariableInit(&inj_state->wait_point); + } + LWLockRelease(AddinShmemInitLock); +} =20 /* Set of callbacks available at point creation */ void @@ -39,6 +82,20 @@ test_injection_notice(const char *name) elog(NOTICE, "notice triggered for injection point %s", name); } =20 +void +test_injection_wait(const char *name) +{ + if (inj_state =3D=3D NULL) + test_injection_init_shmem(); + if (test_injection_wait_event =3D=3D 0) + test_injection_wait_event =3D WaitEventExtensionNew("test_injection_wait= "); + + /* And sleep.. */ + ConditionVariablePrepareToSleep(&inj_state->wait_point); + ConditionVariableSleep(&inj_state->wait_point, test_injection_wait_event); + ConditionVariableCancelSleep(); +} + /* * SQL function for creating an injection point. */ @@ -54,6 +111,8 @@ test_injection_points_attach(PG_FUNCTION_ARGS) function =3D "test_injection_error"; else if (strcmp(mode, "notice") =3D=3D 0) function =3D "test_injection_notice"; + else if (strcmp(mode, "wait") =3D=3D 0) + function =3D "test_injection_wait"; else elog(ERROR, "incorrect mode \"%s\" for injection point creation", mode); =20 @@ -76,6 +135,20 @@ test_injection_points_run(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } =20 +/* + * SQL function for waking a condition variable. + */ +PG_FUNCTION_INFO_V1(test_injection_points_wake); +Datum +test_injection_points_wake(PG_FUNCTION_ARGS) +{ + if (inj_state =3D=3D NULL) + test_injection_init_shmem(); + + ConditionVariableBroadcast(&inj_state->wait_point); + PG_RETURN_VOID(); +} + /* * SQL function for dropping an injection point. */ --=20 2.43.0 --GwmmnaKI4J/fD1SE-- --h3Jod0fYfP9xsIu0 Content-Type: application/pgp-signature; name="signature.asc" -----BEGIN PGP SIGNATURE----- iQIzBAEBCAAdFiEEG72nH6vTowiyblFKnvQgOdbyQH0FAmV4OakACgkQnvQgOdby QH17Rg/+NT+ADH3VKEf0+Jcae3vqH9jO463NSdKrenz16yCqcegsmAitgFh4iG0o mMJG5fQK4jhRvBm7UFNpEXVtZaRCdi3ScL+8KQV3Z9IfcWRkJgBV92dWZl9gU3PA 7gW7Zh6uI4JXP2oaFyOv3S1oj7AsFNIhgIxmxVYRJ4DkKL3vl2qldJVuT8vTI3xd scwJKlmy0uEMiAlrUIji5+FphfnHtuz4MR3txpdNluZXS5nqJ+dzIN7ZV4mwmhA+ 7uPCBAlDnyMzt0t5nDdETwvY22Jy8ss499E823jOiQxBd9Umq3qYbLtIc09qXUQv 0UFTGKQ5tJyvCfQnLLbQDyjmKcAmUbDjwBHJToNionJ5nygOhstW0QYJbwcKykYh SiRsrEtVaTXse+e4brsZnFyHOE0XA3APQ5d7oSdElkBb6IdF7o5l9ZVzE6wI7ZfV mZA9btlSHzXy5vwTNesWC5MGvSpK9sYjEftCxE+rsBrUWMr/o3XsPbXGSh4AKUgc H3yvmpJj9QvYKzUa4/grlyDX/N4vE936vzgDaVxSK9RLNll1VPNFo72F+r8+wByO 9AhVdI0RtroVcowYVdsNawb5NSmap+MK33vLMMekYS/QsdnBimGpeKfVR8Q2OSLb fxvMIqbeE8jCX19ZmpS/ZSjDyBzwTr9CnzbLWicS2lXDNIwWY5Y= =FjG5 -----END PGP SIGNATURE----- --h3Jod0fYfP9xsIu0--