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 106 : 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 18 : injection_point_init_state(void *ptr)
129 : {
130 18 : InjectionPointSharedState *state = (InjectionPointSharedState *) ptr;
131 :
132 18 : SpinLockInit(&state->lock);
133 18 : memset(state->wait_counts, 0, sizeof(state->wait_counts));
134 18 : memset(state->name, 0, sizeof(state->name));
135 18 : ConditionVariableInit(&state->wait_point);
136 18 : }
137 :
138 : /* Shared memory initialization when loading module */
139 : static void
140 8 : injection_shmem_request(void)
141 : {
142 : Size size;
143 :
144 8 : if (prev_shmem_request_hook)
145 2 : prev_shmem_request_hook();
146 :
147 8 : size = MAXALIGN(sizeof(InjectionPointSharedState));
148 8 : RequestAddinShmemSpace(size);
149 8 : }
150 :
151 : static void
152 8 : injection_shmem_startup(void)
153 : {
154 : bool found;
155 :
156 8 : if (prev_shmem_startup_hook)
157 2 : prev_shmem_startup_hook();
158 :
159 : /* Create or attach to the shared memory state */
160 8 : LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
161 :
162 8 : inj_state = ShmemInitStruct("injection_points",
163 : sizeof(InjectionPointSharedState),
164 : &found);
165 :
166 8 : if (!found)
167 : {
168 : /*
169 : * First time through, so initialize. This is shared with the dynamic
170 : * initialization using a DSM.
171 : */
172 8 : injection_point_init_state(inj_state);
173 : }
174 :
175 8 : LWLockRelease(AddinShmemInitLock);
176 8 : }
177 :
178 : /*
179 : * Initialize shared memory area for this module through DSM.
180 : */
181 : static void
182 32 : injection_init_shmem(void)
183 : {
184 : bool found;
185 :
186 32 : if (inj_state != NULL)
187 0 : return;
188 :
189 32 : 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 256 : injection_point_allowed(InjectionPointCondition *condition)
203 : {
204 256 : bool result = true;
205 :
206 256 : switch (condition->type)
207 : {
208 202 : case INJ_CONDITION_PID:
209 202 : if (MyProcPid != condition->pid)
210 78 : result = false;
211 202 : break;
212 54 : case INJ_CONDITION_ALWAYS:
213 54 : break;
214 : }
215 :
216 256 : 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 122 : injection_notice(const char *name, const void *private_data, void *arg)
265 : {
266 122 : InjectionPointCondition *condition = (InjectionPointCondition *) private_data;
267 122 : char *argstr = (char *) arg;
268 :
269 122 : if (!injection_point_allowed(condition))
270 0 : return;
271 :
272 122 : pgstat_report_inj(name);
273 :
274 122 : if (argstr)
275 4 : elog(NOTICE, "notice triggered for injection point %s (%s)",
276 : name, argstr);
277 : else
278 118 : 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 116 : injection_wait(const char *name, const void *private_data, void *arg)
284 : {
285 116 : uint32 old_wait_counts = 0;
286 116 : int index = -1;
287 116 : uint32 injection_wait_event = 0;
288 116 : InjectionPointCondition *condition = (InjectionPointCondition *) private_data;
289 :
290 116 : if (inj_state == NULL)
291 6 : injection_init_shmem();
292 :
293 116 : if (!injection_point_allowed(condition))
294 78 : return;
295 :
296 38 : 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 38 : injection_wait_event = WaitEventInjectionPointNew(name);
304 :
305 : /*
306 : * Find a free slot to wait for, and register this injection point's name.
307 : */
308 38 : SpinLockAcquire(&inj_state->lock);
309 46 : for (int i = 0; i < INJ_MAX_WAIT; i++)
310 : {
311 46 : if (inj_state->name[i][0] == '\0')
312 : {
313 38 : index = i;
314 38 : strlcpy(inj_state->name[i], name, INJ_NAME_MAXLEN);
315 38 : old_wait_counts = inj_state->wait_counts[i];
316 38 : break;
317 : }
318 : }
319 38 : SpinLockRelease(&inj_state->lock);
320 :
321 38 : 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 38 : ConditionVariablePrepareToSleep(&inj_state->wait_point);
327 : for (;;)
328 48 : {
329 : uint32 new_wait_counts;
330 :
331 86 : SpinLockAcquire(&inj_state->lock);
332 86 : new_wait_counts = inj_state->wait_counts[index];
333 86 : SpinLockRelease(&inj_state->lock);
334 :
335 86 : if (old_wait_counts != new_wait_counts)
336 38 : break;
337 48 : ConditionVariableSleep(&inj_state->wait_point, injection_wait_event);
338 : }
339 38 : ConditionVariableCancelSleep();
340 :
341 : /* Remove this injection point from the waiters. */
342 38 : SpinLockAcquire(&inj_state->lock);
343 38 : inj_state->name[index][0] = '\0';
344 38 : SpinLockRelease(&inj_state->lock);
345 : }
346 :
347 : /*
348 : * SQL function for creating an injection point.
349 : */
350 146 : PG_FUNCTION_INFO_V1(injection_points_attach);
351 : Datum
352 118 : injection_points_attach(PG_FUNCTION_ARGS)
353 : {
354 118 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
355 118 : char *action = text_to_cstring(PG_GETARG_TEXT_PP(1));
356 : char *function;
357 118 : InjectionPointCondition condition = {0};
358 :
359 118 : if (strcmp(action, "error") == 0)
360 32 : function = "injection_error";
361 86 : else if (strcmp(action, "notice") == 0)
362 38 : function = "injection_notice";
363 48 : else if (strcmp(action, "wait") == 0)
364 46 : function = "injection_wait";
365 : else
366 2 : elog(ERROR, "incorrect action \"%s\" for injection point creation", action);
367 :
368 116 : if (injection_point_local)
369 : {
370 68 : condition.type = INJ_CONDITION_PID;
371 68 : condition.pid = MyProcPid;
372 : }
373 :
374 116 : pgstat_report_inj_fixed(1, 0, 0, 0, 0);
375 116 : InjectionPointAttach(name, "injection_points", function, &condition,
376 : sizeof(InjectionPointCondition));
377 :
378 116 : 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 116 : pgstat_create_inj(name);
390 :
391 116 : PG_RETURN_VOID();
392 : }
393 :
394 : /*
395 : * SQL function for loading an injection point.
396 : */
397 66 : PG_FUNCTION_INFO_V1(injection_points_load);
398 : Datum
399 6 : injection_points_load(PG_FUNCTION_ARGS)
400 : {
401 6 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
402 :
403 6 : if (inj_state == NULL)
404 2 : injection_init_shmem();
405 :
406 6 : pgstat_report_inj_fixed(0, 0, 0, 0, 1);
407 6 : INJECTION_POINT_LOAD(name);
408 :
409 6 : PG_RETURN_VOID();
410 : }
411 :
412 : /*
413 : * SQL function for triggering an injection point.
414 : */
415 82 : PG_FUNCTION_INFO_V1(injection_points_run);
416 : Datum
417 62 : injection_points_run(PG_FUNCTION_ARGS)
418 : {
419 : char *name;
420 62 : char *arg = NULL;
421 :
422 62 : if (PG_ARGISNULL(0))
423 2 : PG_RETURN_VOID();
424 60 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
425 :
426 60 : if (!PG_ARGISNULL(1))
427 4 : arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
428 :
429 60 : pgstat_report_inj_fixed(0, 0, 1, 0, 0);
430 60 : INJECTION_POINT(name, arg);
431 :
432 48 : PG_RETURN_VOID();
433 : }
434 :
435 : /*
436 : * SQL function for triggering an injection point from cache.
437 : */
438 68 : PG_FUNCTION_INFO_V1(injection_points_cached);
439 : Datum
440 12 : injection_points_cached(PG_FUNCTION_ARGS)
441 : {
442 : char *name;
443 12 : char *arg = NULL;
444 :
445 12 : if (PG_ARGISNULL(0))
446 2 : PG_RETURN_VOID();
447 10 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
448 :
449 10 : if (!PG_ARGISNULL(1))
450 2 : arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
451 :
452 10 : pgstat_report_inj_fixed(0, 0, 0, 1, 0);
453 10 : INJECTION_POINT_CACHED(name, arg);
454 :
455 10 : PG_RETURN_VOID();
456 : }
457 :
458 : /*
459 : * SQL function for waking up an injection point waiting in injection_wait().
460 : */
461 98 : PG_FUNCTION_INFO_V1(injection_points_wakeup);
462 : Datum
463 42 : injection_points_wakeup(PG_FUNCTION_ARGS)
464 : {
465 42 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
466 42 : int index = -1;
467 :
468 42 : if (inj_state == NULL)
469 6 : injection_init_shmem();
470 :
471 : /* First bump the wait counter for the injection point to wake up */
472 42 : SpinLockAcquire(&inj_state->lock);
473 66 : for (int i = 0; i < INJ_MAX_WAIT; i++)
474 : {
475 64 : if (strcmp(name, inj_state->name[i]) == 0)
476 : {
477 40 : index = i;
478 40 : break;
479 : }
480 : }
481 42 : if (index < 0)
482 : {
483 2 : SpinLockRelease(&inj_state->lock);
484 2 : elog(ERROR, "could not find injection point %s to wake up", name);
485 : }
486 40 : inj_state->wait_counts[index]++;
487 40 : SpinLockRelease(&inj_state->lock);
488 :
489 : /* And broadcast the change to the waiters */
490 40 : ConditionVariableBroadcast(&inj_state->wait_point);
491 40 : PG_RETURN_VOID();
492 : }
493 :
494 : /*
495 : * injection_points_set_local
496 : *
497 : * Track if any injection point created in this process ought to run only
498 : * in this process. Such injection points are detached automatically when
499 : * this process exits. This is useful to make test suites concurrent-safe.
500 : */
501 106 : PG_FUNCTION_INFO_V1(injection_points_set_local);
502 : Datum
503 44 : injection_points_set_local(PG_FUNCTION_ARGS)
504 : {
505 : /* Enable flag to add a runtime condition based on this process ID */
506 44 : injection_point_local = true;
507 :
508 44 : if (inj_state == NULL)
509 18 : injection_init_shmem();
510 :
511 : /*
512 : * Register a before_shmem_exit callback to remove any injection points
513 : * linked to this process.
514 : */
515 44 : before_shmem_exit(injection_points_cleanup, (Datum) 0);
516 :
517 44 : PG_RETURN_VOID();
518 : }
519 :
520 : /*
521 : * SQL function for dropping an injection point.
522 : */
523 116 : PG_FUNCTION_INFO_V1(injection_points_detach);
524 : Datum
525 86 : injection_points_detach(PG_FUNCTION_ARGS)
526 : {
527 86 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
528 :
529 86 : pgstat_report_inj_fixed(0, 1, 0, 0, 0);
530 86 : if (!InjectionPointDetach(name))
531 2 : elog(ERROR, "could not detach injection point \"%s\"", name);
532 :
533 : /* Remove point from local list, if required */
534 84 : if (inj_list_local != NIL)
535 : {
536 : MemoryContext oldctx;
537 :
538 24 : oldctx = MemoryContextSwitchTo(TopMemoryContext);
539 24 : inj_list_local = list_delete(inj_list_local, makeString(name));
540 24 : MemoryContextSwitchTo(oldctx);
541 : }
542 :
543 : /* Remove stats entry */
544 84 : pgstat_drop_inj(name);
545 :
546 84 : PG_RETURN_VOID();
547 : }
548 :
549 : /*
550 : * SQL function for listing all the injection points attached.
551 : */
552 66 : PG_FUNCTION_INFO_V1(injection_points_list);
553 : Datum
554 4 : injection_points_list(PG_FUNCTION_ARGS)
555 : {
556 : #define NUM_INJECTION_POINTS_LIST 3
557 4 : ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
558 : List *inj_points;
559 : ListCell *lc;
560 :
561 : /* Build a tuplestore to return our results in */
562 4 : InitMaterializedSRF(fcinfo, 0);
563 :
564 4 : inj_points = InjectionPointList();
565 :
566 10 : foreach(lc, inj_points)
567 : {
568 : Datum values[NUM_INJECTION_POINTS_LIST];
569 : bool nulls[NUM_INJECTION_POINTS_LIST];
570 6 : InjectionPointData *inj_point = lfirst(lc);
571 :
572 6 : memset(values, 0, sizeof(values));
573 6 : memset(nulls, 0, sizeof(nulls));
574 :
575 6 : values[0] = PointerGetDatum(cstring_to_text(inj_point->name));
576 6 : values[1] = PointerGetDatum(cstring_to_text(inj_point->library));
577 6 : values[2] = PointerGetDatum(cstring_to_text(inj_point->function));
578 :
579 : /* shove row into tuplestore */
580 6 : tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
581 : }
582 :
583 4 : return (Datum) 0;
584 : #undef NUM_INJECTION_POINTS_LIST
585 : }
586 :
587 :
588 : void
589 106 : _PG_init(void)
590 : {
591 106 : if (!process_shared_preload_libraries_in_progress)
592 98 : return;
593 :
594 8 : DefineCustomBoolVariable("injection_points.stats",
595 : "Enables statistics for injection points.",
596 : NULL,
597 : &inj_stats_enabled,
598 : false,
599 : PGC_POSTMASTER,
600 : 0,
601 : NULL,
602 : NULL,
603 : NULL);
604 :
605 8 : MarkGUCPrefixReserved("injection_points");
606 :
607 : /* Shared memory initialization */
608 8 : prev_shmem_request_hook = shmem_request_hook;
609 8 : shmem_request_hook = injection_shmem_request;
610 8 : prev_shmem_startup_hook = shmem_startup_hook;
611 8 : shmem_startup_hook = injection_shmem_startup;
612 :
613 8 : pgstat_register_inj();
614 8 : pgstat_register_inj_fixed();
615 : }
|