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-2026, 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 "miscadmin.h"
23 : #include "nodes/pg_list.h"
24 : #include "nodes/value.h"
25 : #include "storage/condition_variable.h"
26 : #include "storage/dsm_registry.h"
27 : #include "storage/ipc.h"
28 : #include "storage/lwlock.h"
29 : #include "storage/shmem.h"
30 : #include "utils/builtins.h"
31 : #include "utils/guc.h"
32 : #include "utils/injection_point.h"
33 : #include "utils/memutils.h"
34 : #include "utils/tuplestore.h"
35 : #include "utils/wait_event.h"
36 :
37 186 : 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 : static void injection_shmem_request(void *arg);
111 : static void injection_shmem_init(void *arg);
112 :
113 : static const ShmemCallbacks injection_shmem_callbacks = {
114 : .request_fn = injection_shmem_request,
115 : .init_fn = injection_shmem_init,
116 : };
117 :
118 : /*
119 : * Routine for shared memory area initialization, used as a callback
120 : * when initializing dynamically with a DSM or when loading the module.
121 : */
122 : static void
123 15 : injection_point_init_state(void *ptr, void *arg)
124 : {
125 15 : InjectionPointSharedState *state = (InjectionPointSharedState *) ptr;
126 :
127 15 : SpinLockInit(&state->lock);
128 15 : memset(state->wait_counts, 0, sizeof(state->wait_counts));
129 15 : memset(state->name, 0, sizeof(state->name));
130 15 : ConditionVariableInit(&state->wait_point);
131 15 : }
132 :
133 : static void
134 3 : injection_shmem_request(void *arg)
135 : {
136 3 : ShmemRequestStruct(.name = "injection_points",
137 : .size = sizeof(InjectionPointSharedState),
138 : .ptr = (void **) &inj_state,
139 : );
140 3 : }
141 :
142 : static void
143 3 : injection_shmem_init(void *arg)
144 : {
145 : /*
146 : * First time through, so initialize. This is shared with the dynamic
147 : * initialization using a DSM.
148 : */
149 3 : injection_point_init_state(inj_state, NULL);
150 3 : }
151 :
152 : /*
153 : * Initialize shared memory area for this module through DSM.
154 : */
155 : static void
156 124 : injection_init_shmem(void)
157 : {
158 : bool found;
159 :
160 124 : if (inj_state != NULL)
161 0 : return;
162 :
163 124 : inj_state = GetNamedDSMSegment("injection_points",
164 : sizeof(InjectionPointSharedState),
165 : injection_point_init_state,
166 : &found, NULL);
167 : }
168 :
169 : /*
170 : * Check runtime conditions associated to an injection point.
171 : *
172 : * Returns true if the named injection point is allowed to run, and false
173 : * otherwise.
174 : */
175 : static bool
176 275 : injection_point_allowed(const InjectionPointCondition *condition)
177 : {
178 275 : bool result = true;
179 :
180 275 : switch (condition->type)
181 : {
182 244 : case INJ_CONDITION_PID:
183 244 : if (MyProcPid != condition->pid)
184 139 : result = false;
185 244 : break;
186 31 : case INJ_CONDITION_ALWAYS:
187 31 : break;
188 : }
189 :
190 275 : return result;
191 : }
192 :
193 : /*
194 : * before_shmem_exit callback to remove injection points linked to a
195 : * specific process.
196 : */
197 : static void
198 63 : injection_points_cleanup(int code, Datum arg)
199 : {
200 : ListCell *lc;
201 :
202 : /* Leave if nothing is tracked locally */
203 63 : if (!injection_point_local)
204 0 : return;
205 :
206 : /* Detach all the local points */
207 188 : foreach(lc, inj_list_local)
208 : {
209 125 : char *name = strVal(lfirst(lc));
210 :
211 125 : (void) InjectionPointDetach(name);
212 : }
213 : }
214 :
215 : /* Set of callbacks available to be attached to an injection point. */
216 : void
217 10 : injection_error(const char *name, const void *private_data, void *arg)
218 : {
219 10 : const InjectionPointCondition *condition = private_data;
220 10 : char *argstr = arg;
221 :
222 10 : if (!injection_point_allowed(condition))
223 0 : return;
224 :
225 10 : if (argstr)
226 1 : elog(ERROR, "error triggered for injection point %s (%s)",
227 : name, argstr);
228 : else
229 9 : elog(ERROR, "error triggered for injection point %s", name);
230 : }
231 :
232 : void
233 58 : injection_notice(const char *name, const void *private_data, void *arg)
234 : {
235 58 : const InjectionPointCondition *condition = private_data;
236 58 : char *argstr = arg;
237 :
238 58 : if (!injection_point_allowed(condition))
239 0 : return;
240 :
241 58 : if (argstr)
242 3 : elog(NOTICE, "notice triggered for injection point %s (%s)",
243 : name, argstr);
244 : else
245 55 : elog(NOTICE, "notice triggered for injection point %s", name);
246 : }
247 :
248 : /* Wait on a condition variable, awaken by injection_points_wakeup() */
249 : void
250 207 : injection_wait(const char *name, const void *private_data, void *arg)
251 : {
252 207 : uint32 old_wait_counts = 0;
253 207 : int index = -1;
254 207 : uint32 injection_wait_event = 0;
255 207 : const InjectionPointCondition *condition = private_data;
256 :
257 207 : if (inj_state == NULL)
258 27 : injection_init_shmem();
259 :
260 207 : if (!injection_point_allowed(condition))
261 139 : return;
262 :
263 : /*
264 : * Use the injection point name for this custom wait event. Note that
265 : * this custom wait event name is not released, but we don't care much for
266 : * testing as this should be short-lived.
267 : */
268 68 : injection_wait_event = WaitEventInjectionPointNew(name);
269 :
270 : /*
271 : * Find a free slot to wait for, and register this injection point's name.
272 : */
273 68 : SpinLockAcquire(&inj_state->lock);
274 91 : for (int i = 0; i < INJ_MAX_WAIT; i++)
275 : {
276 91 : if (inj_state->name[i][0] == '\0')
277 : {
278 68 : index = i;
279 68 : strlcpy(inj_state->name[i], name, INJ_NAME_MAXLEN);
280 68 : old_wait_counts = inj_state->wait_counts[i];
281 68 : break;
282 : }
283 : }
284 68 : SpinLockRelease(&inj_state->lock);
285 :
286 68 : if (index < 0)
287 0 : elog(ERROR, "could not find free slot for wait of injection point %s ",
288 : name);
289 :
290 : /* And sleep.. */
291 68 : ConditionVariablePrepareToSleep(&inj_state->wait_point);
292 : for (;;)
293 97 : {
294 : uint32 new_wait_counts;
295 :
296 165 : SpinLockAcquire(&inj_state->lock);
297 165 : new_wait_counts = inj_state->wait_counts[index];
298 165 : SpinLockRelease(&inj_state->lock);
299 :
300 165 : if (old_wait_counts != new_wait_counts)
301 66 : break;
302 99 : ConditionVariableSleep(&inj_state->wait_point, injection_wait_event);
303 : }
304 66 : ConditionVariableCancelSleep();
305 :
306 : /* Remove this injection point from the waiters. */
307 66 : SpinLockAcquire(&inj_state->lock);
308 66 : inj_state->name[index][0] = '\0';
309 66 : SpinLockRelease(&inj_state->lock);
310 : }
311 :
312 : /*
313 : * SQL function for creating an injection point.
314 : */
315 135 : PG_FUNCTION_INFO_V1(injection_points_attach);
316 : Datum
317 112 : injection_points_attach(PG_FUNCTION_ARGS)
318 : {
319 112 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
320 112 : char *action = text_to_cstring(PG_GETARG_TEXT_PP(1));
321 : char *function;
322 112 : InjectionPointCondition condition = {0};
323 :
324 112 : if (strcmp(action, "error") == 0)
325 17 : function = "injection_error";
326 95 : else if (strcmp(action, "notice") == 0)
327 18 : function = "injection_notice";
328 77 : else if (strcmp(action, "wait") == 0)
329 76 : function = "injection_wait";
330 : else
331 1 : elog(ERROR, "incorrect action \"%s\" for injection point creation", action);
332 :
333 111 : if (injection_point_local)
334 : {
335 77 : condition.type = INJ_CONDITION_PID;
336 77 : condition.pid = MyProcPid;
337 : }
338 :
339 111 : InjectionPointAttach(name, "injection_points", function, &condition,
340 : sizeof(InjectionPointCondition));
341 :
342 111 : if (injection_point_local)
343 : {
344 : MemoryContext oldctx;
345 :
346 : /* Local injection point, so track it for automated cleanup */
347 77 : oldctx = MemoryContextSwitchTo(TopMemoryContext);
348 77 : inj_list_local = lappend(inj_list_local, makeString(pstrdup(name)));
349 77 : MemoryContextSwitchTo(oldctx);
350 : }
351 :
352 111 : PG_RETURN_VOID();
353 : }
354 :
355 : /*
356 : * SQL function for creating an injection point with library name, function
357 : * name and private data.
358 : */
359 43 : PG_FUNCTION_INFO_V1(injection_points_attach_func);
360 : Datum
361 8 : injection_points_attach_func(PG_FUNCTION_ARGS)
362 : {
363 : char *name;
364 : char *lib_name;
365 : char *function;
366 8 : bytea *private_data = NULL;
367 8 : int private_data_size = 0;
368 :
369 8 : if (PG_ARGISNULL(0))
370 1 : elog(ERROR, "injection point name cannot be NULL");
371 7 : if (PG_ARGISNULL(1))
372 1 : elog(ERROR, "injection point library cannot be NULL");
373 6 : if (PG_ARGISNULL(2))
374 1 : elog(ERROR, "injection point function cannot be NULL");
375 :
376 5 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
377 5 : lib_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
378 5 : function = text_to_cstring(PG_GETARG_TEXT_PP(2));
379 :
380 5 : if (!PG_ARGISNULL(3))
381 : {
382 1 : private_data = PG_GETARG_BYTEA_PP(3);
383 1 : private_data_size = VARSIZE_ANY_EXHDR(private_data);
384 : }
385 :
386 5 : if (private_data != NULL)
387 1 : InjectionPointAttach(name, lib_name, function, VARDATA_ANY(private_data),
388 : private_data_size);
389 : else
390 4 : InjectionPointAttach(name, lib_name, function, NULL,
391 : 0);
392 1 : PG_RETURN_VOID();
393 : }
394 :
395 : /*
396 : * SQL function for loading an injection point.
397 : */
398 43 : PG_FUNCTION_INFO_V1(injection_points_load);
399 : Datum
400 2 : injection_points_load(PG_FUNCTION_ARGS)
401 : {
402 2 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
403 :
404 2 : if (inj_state == NULL)
405 1 : injection_init_shmem();
406 :
407 2 : INJECTION_POINT_LOAD(name);
408 :
409 2 : PG_RETURN_VOID();
410 : }
411 :
412 : /*
413 : * SQL function for triggering an injection point.
414 : */
415 48 : PG_FUNCTION_INFO_V1(injection_points_run);
416 : Datum
417 28 : injection_points_run(PG_FUNCTION_ARGS)
418 : {
419 : char *name;
420 28 : char *arg = NULL;
421 :
422 28 : if (PG_ARGISNULL(0))
423 1 : PG_RETURN_VOID();
424 27 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
425 :
426 27 : if (!PG_ARGISNULL(1))
427 2 : arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
428 :
429 27 : INJECTION_POINT(name, arg);
430 :
431 21 : PG_RETURN_VOID();
432 : }
433 :
434 : /*
435 : * SQL function for triggering an injection point from cache.
436 : */
437 44 : PG_FUNCTION_INFO_V1(injection_points_cached);
438 : Datum
439 5 : injection_points_cached(PG_FUNCTION_ARGS)
440 : {
441 : char *name;
442 5 : char *arg = NULL;
443 :
444 5 : if (PG_ARGISNULL(0))
445 1 : PG_RETURN_VOID();
446 4 : name = text_to_cstring(PG_GETARG_TEXT_PP(0));
447 :
448 4 : if (!PG_ARGISNULL(1))
449 1 : arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
450 :
451 4 : INJECTION_POINT_CACHED(name, arg);
452 :
453 4 : PG_RETURN_VOID();
454 : }
455 :
456 : /*
457 : * SQL function for waking up an injection point waiting in injection_wait().
458 : */
459 111 : PG_FUNCTION_INFO_V1(injection_points_wakeup);
460 : Datum
461 72 : injection_points_wakeup(PG_FUNCTION_ARGS)
462 : {
463 72 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
464 72 : int index = -1;
465 :
466 72 : if (inj_state == NULL)
467 51 : injection_init_shmem();
468 :
469 : /* First bump the wait counter for the injection point to wake up */
470 72 : SpinLockAcquire(&inj_state->lock);
471 103 : for (int i = 0; i < INJ_MAX_WAIT; i++)
472 : {
473 102 : if (strcmp(name, inj_state->name[i]) == 0)
474 : {
475 71 : index = i;
476 71 : break;
477 : }
478 : }
479 72 : if (index < 0)
480 : {
481 1 : SpinLockRelease(&inj_state->lock);
482 1 : elog(ERROR, "could not find injection point %s to wake up", name);
483 : }
484 71 : inj_state->wait_counts[index]++;
485 71 : SpinLockRelease(&inj_state->lock);
486 :
487 : /* And broadcast the change to the waiters */
488 71 : ConditionVariableBroadcast(&inj_state->wait_point);
489 71 : PG_RETURN_VOID();
490 : }
491 :
492 : /*
493 : * injection_points_set_local
494 : *
495 : * Track if any injection point created in this process ought to run only
496 : * in this process. Such injection points are detached automatically when
497 : * this process exits. This is useful to make test suites concurrent-safe.
498 : */
499 105 : PG_FUNCTION_INFO_V1(injection_points_set_local);
500 : Datum
501 63 : injection_points_set_local(PG_FUNCTION_ARGS)
502 : {
503 : /* Enable flag to add a runtime condition based on this process ID */
504 63 : injection_point_local = true;
505 :
506 63 : if (inj_state == NULL)
507 45 : injection_init_shmem();
508 :
509 : /*
510 : * Register a before_shmem_exit callback to remove any injection points
511 : * linked to this process.
512 : */
513 63 : before_shmem_exit(injection_points_cleanup, (Datum) 0);
514 :
515 63 : PG_RETURN_VOID();
516 : }
517 :
518 : /*
519 : * SQL function for dropping an injection point.
520 : */
521 118 : PG_FUNCTION_INFO_V1(injection_points_detach);
522 : Datum
523 93 : injection_points_detach(PG_FUNCTION_ARGS)
524 : {
525 93 : char *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
526 :
527 93 : if (!InjectionPointDetach(name))
528 1 : elog(ERROR, "could not detach injection point \"%s\"", name);
529 :
530 : /* Remove point from local list, if required */
531 92 : if (inj_list_local != NIL)
532 : {
533 : MemoryContext oldctx;
534 :
535 14 : oldctx = MemoryContextSwitchTo(TopMemoryContext);
536 14 : inj_list_local = list_delete(inj_list_local, makeString(name));
537 14 : MemoryContextSwitchTo(oldctx);
538 : }
539 :
540 92 : PG_RETURN_VOID();
541 : }
542 :
543 : /*
544 : * SQL function for listing all the injection points attached.
545 : */
546 44 : PG_FUNCTION_INFO_V1(injection_points_list);
547 : Datum
548 3 : injection_points_list(PG_FUNCTION_ARGS)
549 : {
550 : #define NUM_INJECTION_POINTS_LIST 3
551 3 : ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
552 : List *inj_points;
553 : ListCell *lc;
554 :
555 : /* Build a tuplestore to return our results in */
556 3 : InitMaterializedSRF(fcinfo, 0);
557 :
558 3 : inj_points = InjectionPointList();
559 :
560 7 : foreach(lc, inj_points)
561 : {
562 : Datum values[NUM_INJECTION_POINTS_LIST];
563 : bool nulls[NUM_INJECTION_POINTS_LIST];
564 4 : InjectionPointData *inj_point = lfirst(lc);
565 :
566 4 : memset(values, 0, sizeof(values));
567 4 : memset(nulls, 0, sizeof(nulls));
568 :
569 4 : values[0] = PointerGetDatum(cstring_to_text(inj_point->name));
570 4 : values[1] = PointerGetDatum(cstring_to_text(inj_point->library));
571 4 : values[2] = PointerGetDatum(cstring_to_text(inj_point->function));
572 :
573 : /* shove row into tuplestore */
574 4 : tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
575 : }
576 :
577 3 : return (Datum) 0;
578 : #undef NUM_INJECTION_POINTS_LIST
579 : }
580 :
581 : void
582 186 : _PG_init(void)
583 : {
584 186 : if (!process_shared_preload_libraries_in_progress)
585 183 : return;
586 :
587 3 : RegisterShmemCallbacks(&injection_shmem_callbacks);
588 : }
|