From 1e11c0400bf802834792f3e9c897b17a3d14bca1 Mon Sep 17 00:00:00 2001 From: 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 +/* 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 diff --git a/src/include/utils/injection_point.h b/src/include/utils/injection_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" @@ -147,6 +148,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, AsyncShmemSize()); size = add_size(size, StatsShmemSize()); size = add_size(size, WaitEventExtensionShmemSize()); + size = add_size(size, InjectionPointShmemSize()); #ifdef EXEC_BACKEND size = add_size(size, ShmemBackendArraySize()); #endif @@ -348,6 +350,7 @@ CreateOrAttachShmemStructs(void) AsyncShmemInit(); StatsShmemInit(); WaitEventExtensionShmemInit(); + InjectionPointShmemInit(); } /* 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_status or update WrapLimitsVacuum "Waiting to update limits on transaction id and multixact consumption." NotifyQueueTail "Waiting to update limit on NOTIFY message storage." WaitEventExtension "Waiting to read or update custom wait events information for extensions." +InjectionPoint "Waiting to read or update information related to injection points." 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/Makefile index c2971c7678..d9f59785b9 100644 --- a/src/backend/utils/misc/Makefile +++ b/src/backend/utils/misc/Makefile @@ -21,6 +21,7 @@ OBJS = \ 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/misc/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 = 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 == NULL) + { + HASHCTL hash_ctl; + + hash_ctl.keysize = sizeof(char[INJ_NAME_MAXLEN]); + hash_ctl.entrysize = sizeof(InjectionPointCacheEntry); + hash_ctl.hcxt = TopMemoryContext; + + InjectionPointCache = hash_create("InjectionPoint cache hash", + INJECTION_POINT_HASH_MAX_SIZE, + &hash_ctl, + HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); + } + + entry = (InjectionPointCacheEntry *) + hash_search(InjectionPointCache, name, HASH_ENTER, &found); + + if (!found) + { + memcpy(entry->name, name, strlen(name)); + entry->callback = 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 == 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 == NULL) + return NULL; + + entry = (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 = 0; + + sz = 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 = sizeof(char[INJ_NAME_MAXLEN]); + info.entrysize = sizeof(InjectionPointEntry); + InjectionPointHash = 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 != NULL); + if (stat(name, &st) == 0) + return !S_ISDIR(st.st_mode); + else if (!(errno == ENOENT || errno == 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) >= INJ_NAME_MAXLEN) + elog(ERROR, "injection point name %s too long", name); + if (strlen(library) >= INJ_LIB_MAXLEN) + elog(ERROR, "injection point library %s too long", library); + if (strlen(function) >= 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 = (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] = '\0'; + memcpy(entry_by_name->library, library, sizeof(entry_by_name->library)); + entry_by_name->library[INJ_LIB_MAXLEN - 1] = '\0'; + memcpy(entry_by_name->function, function, sizeof(entry_by_name->function)); + entry_by_name->function[INJ_FUNC_MAXLEN - 1] = '\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 = (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 = injection_point_cache_get(name); + if (injection_callback == 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 = (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/meson.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 += 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: + + + + + 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 default. + See for more details. + This option is only for developers to test specific concurrency + scenarios. + + + + @@ -3184,6 +3199,21 @@ ninja install + + + + + 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 default. + See for more details. + 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) + + 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 + from Meson. + + + Using C++ for Extensibility 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 +# +# Injection points +# + + +# Check whether --enable-injection-points was given. +if test "${enable_injection_points+set}" = set; then : + enableval=$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 option" "$LINENO" 5 + ;; + esac + +else + enable_injection_points=no + +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) +# +# 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 = find_program(meson_binpath, native: true) ############################################################### cdata.set('USE_ASSERT_CHECKING', get_option('cassert') ? 1 : false) +cdata.set('USE_INJECTION_POINTS', get_option('injection_points') ? 1 : false) blocksize = get_option('blocksize').to_int() * 1024 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') +option('injection_points', type: 'boolean', value: false, + description: 'Enable injection points') + option('PG_TEST_EXTRA', type: 'string', value: '', description: 'Enable selected extra tests') 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 = @enable_nls@ enable_debug = @enable_debug@ enable_dtrace = @enable_dtrace@ enable_coverage = @enable_coverage@ +enable_injection_points = @enable_injection_points@ enable_tap_tests = @enable_tap_tests@ python_includespec = @python_includespec@ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list 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 -- 2.43.0