Line data Source code
1 : /*--------------------------------------------------------------------------
2 : *
3 : * injection_points.c
4 : * Code for testing injection points.
5 : *
6 : * Injection points are able to trigger user-defined callbacks in pre-defined
7 : * code paths.
8 : *
9 : * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
10 : * Portions Copyright (c) 1994, Regents of the University of California
11 : *
12 : * IDENTIFICATION
13 : * src/test/modules/injection_points/injection_points.c
14 : *
15 : * -------------------------------------------------------------------------
16 : */
17 :
18 : #include "postgres.h"
19 :
20 : #include "fmgr.h"
21 : #include "funcapi.h"
22 : #include "injection_stats.h"
23 : #include "miscadmin.h"
24 : #include "nodes/pg_list.h"
25 : #include "nodes/value.h"
26 : #include "storage/condition_variable.h"
27 : #include "storage/dsm_registry.h"
28 : #include "storage/ipc.h"
29 : #include "storage/lwlock.h"
30 : #include "storage/shmem.h"
31 : #include "utils/builtins.h"
32 : #include "utils/guc.h"
33 : #include "utils/injection_point.h"
34 : #include "utils/memutils.h"
35 : #include "utils/wait_event.h"
36 :
37 110 : PG_MODULE_MAGIC;
38 :
39 : /* Maximum number of waits usable in injection points at once */
40 : #define INJ_MAX_WAIT 8
41 : #define INJ_NAME_MAXLEN 64
42 :
43 : /*
44 : * Conditions related to injection points. This tracks in shared memory the
45 : * runtime conditions under which an injection point is allowed to run,
46 : * stored as private_data when an injection point is attached, and passed as
47 : * argument to the callback.
48 : *
49 : * If more types of runtime conditions need to be tracked, this structure
50 : * should be expanded.
51 : */
52 : typedef enum InjectionPointConditionType
53 : {
54 : INJ_CONDITION_ALWAYS = 0, /* always run */
55 : INJ_CONDITION_PID, /* PID restriction */
56 : } InjectionPointConditionType;
57 :
58 : typedef struct InjectionPointCondition
59 : {
60 : /* Type of the condition */
61 : InjectionPointConditionType type;
62 :
63 : /* ID of the process where the injection point is allowed to run */
64 : int pid;
65 : } InjectionPointCondition;
66 :
67 : /*
68 : * List of injection points stored in TopMemoryContext attached
69 : * locally to this process.
70 : */
71 : static List *inj_list_local = NIL;
72 :
73 : /*
74 : * Shared state information for injection points.
75 : *
76 : * This state data can be initialized in two ways: dynamically with a DSM
77 : * or when loading the module.
78 : */
79 : typedef struct InjectionPointSharedState
80 : {
81 : /* Protects access to other fields */
82 : slock_t lock;
83 :
84 : /* Counters advancing when injection_points_wakeup() is called */
85 : uint32 wait_counts[INJ_MAX_WAIT];
86 :
87 : /* Names of injection points attached to wait counters */
88 : char name[INJ_MAX_WAIT][INJ_NAME_MAXLEN];
89 :
90 : /* Condition variable used for waits and wakeups */
91 : ConditionVariable wait_point;
92 : } InjectionPointSharedState;
93 :
94 : /* Pointer to shared-memory state. */
95 : static InjectionPointSharedState *inj_state = NULL;
96 :
97 : extern PGDLLEXPORT void injection_error(const char *name,
98 : const void *private_data,
99 : void *arg);
100 : extern PGDLLEXPORT void injection_notice(const char *name,
101 : const void *private_data,
102 : void *arg);
103 : extern PGDLLEXPORT void injection_wait(const char *name,
104 : const void *private_data,
105 : void *arg);
106 :
107 : /* track if injection points attached in this process are linked to it */
108 : static bool injection_point_local = false;
109 :
110 : /*
111 : * GUC variable
112 : *
113 : * This GUC is useful to control if statistics should be enabled or not
114 : * during a test with injection points, like for example if a test relies
115 : * on a callback run in a critical section where no allocation should happen.
116 : */
117 : bool inj_stats_enabled = false;
118 :
119 : /* Shared memory init callbacks */
120 : static shmem_request_hook_type prev_shmem_request_hook = NULL;
121 : static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
122 :
123 : /*
124 : * Routine for shared memory area initialization, used as a callback
125 : * when initializing dynamically with a DSM or when loading the module.
126 : */
127 : static void
128 20 : injection_point_init_state(void *ptr)
129 : {
130 20 : InjectionPointSharedState *state = (InjectionPointSharedState *) ptr;
131 :
132 20 : SpinLockInit(&state->lock);
133 20 : memset(state->wait_counts, 0, sizeof(state->wait_counts));
134 20 : memset(state->name, 0, sizeof(state->name));
135 20 : ConditionVariableInit(&state->wait_point);
136 20 : }
137 :
138 : /* Shared memory initialization when loading module */
139 : static void
140 10 : injection_shmem_request(void)
141 : {
142 : Size size;
143 :
144 10 : if (prev_shmem_request_hook)
145 2 : prev_shmem_request_hook();
146 :
147 10 : size = MAXALIGN(sizeof(InjectionPointSharedState));
148 10 : RequestAddinShmemSpace(size);
149 10 : }
150 :
151 : static void
152 10 : injection_shmem_startup(void)
153 : {
154 : bool found;
155 :
156 10 : if (prev_shmem_startup_hook)
157 2 : prev_shmem_startup_hook();
158 :
159 : /* Create or attach to the shared memory state */
160 10 : LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
161 :
162 10 : inj_state = ShmemInitStruct("injection_points",
163 : sizeof(InjectionPointSharedState),
164 : &found);
165 :
166 10 : if (!found)
167 : {
168 : /*
169 : * First time through, so initialize. This is shared with the dynamic
170 : * initialization using a DSM.
171 : */
172 10 : injection_point_init_state(inj_state);
173 : }
174 :
175 10 : LWLockRelease(AddinShmemInitLock);
176 10 : }
177 :
178 : /*
179 : * Initialize shared memory area for this module through DSM.
180 : */
181 : static void
182 34 : injection_init_shmem(void)
183 : {
184 : bool found;
185 :
186 34 : if (inj_state != NULL)
187 0 : return;
188 :
189 34 : inj_state = GetNamedDSMSegment("injection_points",
190 : sizeof(InjectionPointSharedState),
191 : injection_point_init_state,
192 : &found);
193 : }
194 :
195 : /*
196 : * Check runtime conditions associated to an injection point.
197 : *
198 : * Returns true if the named injection point is allowed to run, and false
199 : * otherwise.
200 : */
201 : static bool
202 262 : injection_point_allowed(InjectionPointCondition *condition)
203 : {
204 262 : bool result = true;
205 :
206 262 : switch (condition->type)
207 : {
208 202 : case INJ_CONDITION_PID:
209 202 : if (MyProcPid != condition->pid)
210 78 : result = false;
211 202 : break;
212 60 : case INJ_CONDITION_ALWAYS:
213 60 : break;
214 : }
215 :
216 262 : return result;
217 : }
218 :
219 : /*
220 : * before_shmem_exit callback to remove injection points linked to a
221 : * specific process.
222 : */
223 : static void
224 44 : injection_points_cleanup(int code, Datum arg)
225 : {
226 : ListCell *lc;
227 :
228 : /* Leave if nothing is tracked locally */
229 44 : if (!injection_point_local)
230 0 : return;
231 :
232 : /* Detach all the local points */
233 208 : foreach(lc, inj_list_local)
234 : {
235 164 : char *name = strVal(lfirst(lc));
236 :
237 164 : (void) InjectionPointDetach(name);
238 :
239 : /* Remove stats entry */
240 164 : pgstat_drop_inj(name);
241 : }
242 : }
243 :
244 : /* Set of callbacks available to be attached to an injection point. */
245 : void
246 18 : injection_error(const char *name, const void *private_data, void *arg)
247 : {
248 18 : InjectionPointCondition *condition = (InjectionPointCondition *) private_data;
249 18 : char *argstr = (char *) arg;
250 :
251 18 : if (!injection_point_allowed(condition))
252 0 : return;
253 :
254 18 : pgstat_report_inj(name);
255 :
256 18 : if (argstr)
257 2 : elog(ERROR, "error triggered for injection point %s (%s)",
258 : name, argstr);
259 : else
260 16 : elog(ERROR, "error triggered for injection point %s", name);
261 : }
262 :
263 : void
264 124 : injection_notice(const char *name, const void *private_data, void *arg)
265 : {
266 124 : InjectionPointCondition *condition = (InjectionPointCondition *) private_data;
267 124 : char *argstr = (char *) arg;
268 :
269 124 : if (!injection_point_allowed(condition))
270 0 : return;
271 :
272 124 : pgstat_report_inj(name);
273 :
274 124 : if (argstr)
275 4 : elog(NOTICE, "notice triggered for injection point %s (%s)",
276 : name, argstr);
277 : else
278 120 : elog(NOTICE, "notice triggered for injection point %s", name);
279 : }
280 :
281 : /* Wait on a condition variable, awaken by injection_points_wakeup() */
282 : void
283 120 : injection_wait(const char *name, const void *private_data, void *arg)
284 : {
285 120 : uint32 old_wait_counts = 0;
286 120 : int index = -1;
287 120 : uint32 injection_wait_event = 0;
288 120 : InjectionPointCondition *condition = (InjectionPointCondition *) private_data;
289 :
290 120 : if (inj_state == NULL)
291 8 : injection_init_shmem();
292 :
293 120 : if (!injection_point_allowed(condition))
294 78 : return;
295 :
296 42 : pgstat_report_inj(name);
297 :
298 : /*
299 : * Use the injection point name for this custom wait event. Note that
300 : * this custom wait event name is not released, but we don't care much for
301 : * testing as this should be short-lived.
302 : */
303 42 : injection_wait_event = WaitEventInjectionPointNew(name);
304 :
305 : /*
306 : * Find a free slot to wait for, and register this injection point's name.
307 : */
308 42 : SpinLockAcquire(&inj_state->lock);
309 50 : for (int i = 0; i < INJ_MAX_WAIT; i++)
310 : {
311 50 : if (inj_state->name[i][0] == '\0')
312 : {
313 42 : index = i;
314 42 : strlcpy(inj_state->name[i], name, INJ_NAME_MAXLEN);
315 42 : old_wait_counts = inj_state->wait_counts[i];
316 42 : break;
317 : }
318 : }
319 42 : SpinLockRelease(&inj_state->lock);
320 :
321 42 : if (index < 0)
322 0 : elog(ERROR, "could not find free slot for wait of injection point %s ",
323 : name);
324 :
325 : /* And sleep.. */
326 42 : ConditionVariablePrepareToSleep(&inj_state->wait_point);
327 : for (;;)
328 52 : {
329 : uint32 new_wait_counts;
330 :
331 94 : SpinLockAcquire(&inj_state->lock);
332 94 : new_wait_counts = inj_state->wait_counts[index];
333 94 : SpinLockRelease(&inj_state->lock);
334 :
335 94 : if (old_wait_counts != new_wait_counts)
336 42 : break;
337 52 : ConditionVariableSleep(&inj_state->wait_point, injection_wait_event);
338 : }
339 42 : ConditionVariableCancelSleep();
340 :
341 : /* Remove this injection point from the waiters. */
342 42 : SpinLockAcquire(&inj_state->lock);
343 42 : inj_state->name[index][0] = '\0';
344 42 : SpinLockRelease(&inj_state->lock);
345 : }
346 :
347 : /*
348 : * SQL function for creating an injection point.
349 : */
350 150 : PG_FUNCTION_INFO_V1(injection_points_attach);
351 : Datum
352 122 : injection_points_attach(PG_FUNCTION_ARGS)
353 : {
354 122 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
355 122 : char *action = text_to_cstring(PG_GETARG_TEXT_PP(1));
356 : char *function;
357 122 : InjectionPointCondition condition = {0};
358 :
359 122 : if (strcmp(action, "error") == 0)
360 32 : function = "injection_error";
361 90 : else if (strcmp(action, "notice") == 0)
362 38 : function = "injection_notice";
363 52 : else if (strcmp(action, "wait") == 0)
364 50 : function = "injection_wait";
365 : else
366 2 : elog(ERROR, "incorrect action \"%s\" for injection point creation", action);
367 :
368 120 : if (injection_point_local)
369 : {
370 68 : condition.type = INJ_CONDITION_PID;
371 68 : condition.pid = MyProcPid;
372 : }
373 :
374 120 : pgstat_report_inj_fixed(1, 0, 0, 0, 0);
375 120 : InjectionPointAttach(name, "injection_points", function, &condition,
376 : sizeof(InjectionPointCondition));
377 :
378 120 : if (injection_point_local)
379 : {
380 : MemoryContext oldctx;
381 :
382 : /* Local injection point, so track it for automated cleanup */
383 68 : oldctx = MemoryContextSwitchTo(TopMemoryContext);
384 68 : inj_list_local = lappend(inj_list_local, makeString(pstrdup(name)));
385 68 : MemoryContextSwitchTo(oldctx);
386 : }
387 :
388 : /* Add entry for stats */
389 120 : pgstat_create_inj(name);
390 :
391 120 : PG_RETURN_VOID();
392 : }
393 :
394 : /*
395 : * SQL function for creating an injection point with library name, function
396 : * name and private data.
397 : */
398 64 : PG_FUNCTION_INFO_V1(injection_points_attach_func);
399 : Datum
400 16 : injection_points_attach_func(PG_FUNCTION_ARGS)
401 : {
402 : char *name;
403 : char *lib_name;
404 : char *function;
405 16 : bytea *private_data = NULL;
406 16 : int private_data_size = 0;
407 :
408 16 : if (PG_ARGISNULL(0))
409 2 : elog(ERROR, "injection point name cannot be NULL");
410 14 : if (PG_ARGISNULL(1))
411 2 : elog(ERROR, "injection point library cannot be NULL");
412 12 : if (PG_ARGISNULL(2))
413 2 : elog(ERROR, "injection point function cannot be NULL");
414 :
415 10 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
416 10 : lib_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
417 10 : function = text_to_cstring(PG_GETARG_TEXT_PP(2));
418 :
419 10 : if (!PG_ARGISNULL(3))
420 : {
421 2 : private_data = PG_GETARG_BYTEA_PP(3);
422 2 : private_data_size = VARSIZE_ANY_EXHDR(private_data);
423 : }
424 :
425 10 : pgstat_report_inj_fixed(1, 0, 0, 0, 0);
426 10 : if (private_data != NULL)
427 2 : InjectionPointAttach(name, lib_name, function, VARDATA_ANY(private_data),
428 : private_data_size);
429 : else
430 8 : InjectionPointAttach(name, lib_name, function, NULL,
431 : 0);
432 2 : PG_RETURN_VOID();
433 : }
434 :
435 : /*
436 : * SQL function for loading an injection point.
437 : */
438 66 : PG_FUNCTION_INFO_V1(injection_points_load);
439 : Datum
440 6 : injection_points_load(PG_FUNCTION_ARGS)
441 : {
442 6 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
443 :
444 6 : if (inj_state == NULL)
445 2 : injection_init_shmem();
446 :
447 6 : pgstat_report_inj_fixed(0, 0, 0, 0, 1);
448 6 : INJECTION_POINT_LOAD(name);
449 :
450 6 : PG_RETURN_VOID();
451 : }
452 :
453 : /*
454 : * SQL function for triggering an injection point.
455 : */
456 82 : PG_FUNCTION_INFO_V1(injection_points_run);
457 : Datum
458 64 : injection_points_run(PG_FUNCTION_ARGS)
459 : {
460 : char *name;
461 64 : char *arg = NULL;
462 :
463 64 : if (PG_ARGISNULL(0))
464 2 : PG_RETURN_VOID();
465 62 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
466 :
467 62 : if (!PG_ARGISNULL(1))
468 4 : arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
469 :
470 62 : pgstat_report_inj_fixed(0, 0, 1, 0, 0);
471 62 : INJECTION_POINT(name, arg);
472 :
473 50 : PG_RETURN_VOID();
474 : }
475 :
476 : /*
477 : * SQL function for triggering an injection point from cache.
478 : */
479 68 : PG_FUNCTION_INFO_V1(injection_points_cached);
480 : Datum
481 12 : injection_points_cached(PG_FUNCTION_ARGS)
482 : {
483 : char *name;
484 12 : char *arg = NULL;
485 :
486 12 : if (PG_ARGISNULL(0))
487 2 : PG_RETURN_VOID();
488 10 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
489 :
490 10 : if (!PG_ARGISNULL(1))
491 2 : arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
492 :
493 10 : pgstat_report_inj_fixed(0, 0, 0, 1, 0);
494 10 : INJECTION_POINT_CACHED(name, arg);
495 :
496 10 : PG_RETURN_VOID();
497 : }
498 :
499 : /*
500 : * SQL function for waking up an injection point waiting in injection_wait().
501 : */
502 100 : PG_FUNCTION_INFO_V1(injection_points_wakeup);
503 : Datum
504 44 : injection_points_wakeup(PG_FUNCTION_ARGS)
505 : {
506 44 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
507 44 : int index = -1;
508 :
509 44 : if (inj_state == NULL)
510 6 : injection_init_shmem();
511 :
512 : /* First bump the wait counter for the injection point to wake up */
513 44 : SpinLockAcquire(&inj_state->lock);
514 68 : for (int i = 0; i < INJ_MAX_WAIT; i++)
515 : {
516 66 : if (strcmp(name, inj_state->name[i]) == 0)
517 : {
518 42 : index = i;
519 42 : break;
520 : }
521 : }
522 44 : if (index < 0)
523 : {
524 2 : SpinLockRelease(&inj_state->lock);
525 2 : elog(ERROR, "could not find injection point %s to wake up", name);
526 : }
527 42 : inj_state->wait_counts[index]++;
528 42 : SpinLockRelease(&inj_state->lock);
529 :
530 : /* And broadcast the change to the waiters */
531 42 : ConditionVariableBroadcast(&inj_state->wait_point);
532 42 : PG_RETURN_VOID();
533 : }
534 :
535 : /*
536 : * injection_points_set_local
537 : *
538 : * Track if any injection point created in this process ought to run only
539 : * in this process. Such injection points are detached automatically when
540 : * this process exits. This is useful to make test suites concurrent-safe.
541 : */
542 106 : PG_FUNCTION_INFO_V1(injection_points_set_local);
543 : Datum
544 44 : injection_points_set_local(PG_FUNCTION_ARGS)
545 : {
546 : /* Enable flag to add a runtime condition based on this process ID */
547 44 : injection_point_local = true;
548 :
549 44 : if (inj_state == NULL)
550 18 : injection_init_shmem();
551 :
552 : /*
553 : * Register a before_shmem_exit callback to remove any injection points
554 : * linked to this process.
555 : */
556 44 : before_shmem_exit(injection_points_cleanup, (Datum) 0);
557 :
558 44 : PG_RETURN_VOID();
559 : }
560 :
561 : /*
562 : * SQL function for dropping an injection point.
563 : */
564 118 : PG_FUNCTION_INFO_V1(injection_points_detach);
565 : Datum
566 90 : injection_points_detach(PG_FUNCTION_ARGS)
567 : {
568 90 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
569 :
570 90 : pgstat_report_inj_fixed(0, 1, 0, 0, 0);
571 90 : if (!InjectionPointDetach(name))
572 2 : elog(ERROR, "could not detach injection point \"%s\"", name);
573 :
574 : /* Remove point from local list, if required */
575 88 : if (inj_list_local != NIL)
576 : {
577 : MemoryContext oldctx;
578 :
579 24 : oldctx = MemoryContextSwitchTo(TopMemoryContext);
580 24 : inj_list_local = list_delete(inj_list_local, makeString(name));
581 24 : MemoryContextSwitchTo(oldctx);
582 : }
583 :
584 : /* Remove stats entry */
585 88 : pgstat_drop_inj(name);
586 :
587 88 : PG_RETURN_VOID();
588 : }
589 :
590 : /*
591 : * SQL function for listing all the injection points attached.
592 : */
593 66 : PG_FUNCTION_INFO_V1(injection_points_list);
594 : Datum
595 6 : injection_points_list(PG_FUNCTION_ARGS)
596 : {
597 : #define NUM_INJECTION_POINTS_LIST 3
598 6 : ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
599 : List *inj_points;
600 : ListCell *lc;
601 :
602 : /* Build a tuplestore to return our results in */
603 6 : InitMaterializedSRF(fcinfo, 0);
604 :
605 6 : inj_points = InjectionPointList();
606 :
607 14 : foreach(lc, inj_points)
608 : {
609 : Datum values[NUM_INJECTION_POINTS_LIST];
610 : bool nulls[NUM_INJECTION_POINTS_LIST];
611 8 : InjectionPointData *inj_point = lfirst(lc);
612 :
613 8 : memset(values, 0, sizeof(values));
614 8 : memset(nulls, 0, sizeof(nulls));
615 :
616 8 : values[0] = PointerGetDatum(cstring_to_text(inj_point->name));
617 8 : values[1] = PointerGetDatum(cstring_to_text(inj_point->library));
618 8 : values[2] = PointerGetDatum(cstring_to_text(inj_point->function));
619 :
620 : /* shove row into tuplestore */
621 8 : tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
622 : }
623 :
624 6 : return (Datum) 0;
625 : #undef NUM_INJECTION_POINTS_LIST
626 : }
627 :
628 :
629 : void
630 110 : _PG_init(void)
631 : {
632 110 : if (!process_shared_preload_libraries_in_progress)
633 100 : return;
634 :
635 10 : DefineCustomBoolVariable("injection_points.stats",
636 : "Enables statistics for injection points.",
637 : NULL,
638 : &inj_stats_enabled,
639 : false,
640 : PGC_POSTMASTER,
641 : 0,
642 : NULL,
643 : NULL,
644 : NULL);
645 :
646 10 : MarkGUCPrefixReserved("injection_points");
647 :
648 : /* Shared memory initialization */
649 10 : prev_shmem_request_hook = shmem_request_hook;
650 10 : shmem_request_hook = injection_shmem_request;
651 10 : prev_shmem_startup_hook = shmem_startup_hook;
652 10 : shmem_startup_hook = injection_shmem_startup;
653 :
654 10 : pgstat_register_inj();
655 10 : pgstat_register_inj_fixed();
656 : }
|