Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * pg_stash_advice.c
4 : : * core infrastructure for pg_stash_advice contrib module
5 : : *
6 : : * Copyright (c) 2016-2026, PostgreSQL Global Development Group
7 : : *
8 : : * contrib/pg_stash_advice/pg_stash_advice.c
9 : : *
10 : : *-------------------------------------------------------------------------
11 : : */
12 : : #include "postgres.h"
13 : :
14 : : #include "common/hashfn.h"
15 : : #include "common/string.h"
16 : : #include "miscadmin.h"
17 : : #include "nodes/queryjumble.h"
18 : : #include "pg_plan_advice.h"
19 : : #include "pg_stash_advice.h"
20 : : #include "postmaster/bgworker.h"
21 : : #include "storage/dsm_registry.h"
22 : : #include "utils/guc.h"
23 : : #include "utils/memutils.h"
24 : :
29 rhaas@postgresql.org 25 :GNC 6 : PG_MODULE_MAGIC;
26 : :
27 : : /* Shared memory hash table parameters */
28 : : static dshash_parameters pgsa_stash_dshash_parameters = {
29 : : NAMEDATALEN,
30 : : sizeof(pgsa_stash),
31 : : dshash_strcmp,
32 : : dshash_strhash,
33 : : dshash_strcpy,
34 : : LWTRANCHE_INVALID /* gets set at runtime */
35 : : };
36 : :
37 : : static dshash_parameters pgsa_entry_dshash_parameters = {
38 : : sizeof(pgsa_entry_key),
39 : : sizeof(pgsa_entry),
40 : : dshash_memcmp,
41 : : dshash_memhash,
42 : : dshash_memcpy,
43 : : LWTRANCHE_INVALID /* gets set at runtime */
44 : : };
45 : :
46 : : /* GUC variables */
47 : : static char *pg_stash_advice_stash_name = "";
48 : : bool pg_stash_advice_persist = true;
49 : : int pg_stash_advice_persist_interval = 30;
50 : :
51 : : /* Shared memory pointers */
52 : : pgsa_shared_state *pgsa_state;
53 : : dsa_area *pgsa_dsa_area;
54 : : dshash_table *pgsa_stash_dshash;
55 : : dshash_table *pgsa_entry_dshash;
56 : :
57 : : /* Other global variables */
58 : : static MemoryContext pg_stash_advice_mcxt;
59 : :
60 : : /* Function prototypes */
61 : : static char *pgsa_advisor(PlannerGlobal *glob,
62 : : Query *parse,
63 : : const char *query_string,
64 : : int cursorOptions,
65 : : ExplainState *es);
66 : : static bool pgsa_check_stash_name_guc(char **newval, void **extra,
67 : : GucSource source);
68 : : static void pgsa_init_shared_state(void *ptr, void *arg);
69 : : static bool pgsa_is_identifier(char *str);
70 : :
71 : : /* Stash name -> stash ID hash table */
72 : : #define SH_PREFIX pgsa_stash_name_table
73 : : #define SH_ELEMENT_TYPE pgsa_stash_name
74 : : #define SH_KEY_TYPE uint64
75 : : #define SH_KEY pgsa_stash_id
76 : : #define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) &(key), sizeof(uint64))
77 : : #define SH_EQUAL(tb, a, b) (a == b)
78 : : #define SH_SCOPE extern
79 : : #define SH_DEFINE
80 : : #include "lib/simplehash.h"
81 : :
82 : : /*
83 : : * Initialize this module.
84 : : */
85 : : void
86 : 6 : _PG_init(void)
87 : : {
88 : : void (*add_advisor_fn) (pg_plan_advice_advisor_hook hook);
89 : :
90 : : /* If compute_query_id = 'auto', we would like query IDs. */
91 : 6 : EnableQueryId();
92 : :
93 : : /* Define our GUCs. */
28 94 [ + + ]: 6 : if (process_shared_preload_libraries_in_progress)
95 : 4 : DefineCustomBoolVariable("pg_stash_advice.persist",
96 : : "Save and restore advice stash contents across restarts.",
97 : : NULL,
98 : : &pg_stash_advice_persist,
99 : : true,
100 : : PGC_POSTMASTER,
101 : : 0,
102 : : NULL,
103 : : NULL,
104 : : NULL);
105 : : else
106 : 2 : pg_stash_advice_persist = false;
107 : :
108 : 6 : DefineCustomIntVariable("pg_stash_advice.persist_interval",
109 : : "Interval between advice stash saves, in seconds.",
110 : : NULL,
111 : : &pg_stash_advice_persist_interval,
112 : : 30,
113 : : 0,
114 : : 3600,
115 : : PGC_SIGHUP,
116 : : GUC_UNIT_S,
117 : : NULL,
118 : : NULL,
119 : : NULL);
120 : :
29 121 : 6 : DefineCustomStringVariable("pg_stash_advice.stash_name",
122 : : "Name of the advice stash to be used in this session.",
123 : : NULL,
124 : : &pg_stash_advice_stash_name,
125 : : "",
126 : : PGC_USERSET,
127 : : 0,
128 : : pgsa_check_stash_name_guc,
129 : : NULL,
130 : : NULL);
131 : :
132 : 6 : MarkGUCPrefixReserved("pg_stash_advice");
133 : :
134 : : /* Start the background worker for persistence, if enabled. */
28 135 [ + + ]: 6 : if (pg_stash_advice_persist)
136 : 4 : pgsa_start_worker();
137 : :
138 : : /* Tell pg_plan_advice that we want to provide advice strings. */
29 139 : 6 : add_advisor_fn =
140 : 6 : load_external_function("pg_plan_advice", "pg_plan_advice_add_advisor",
141 : : true, NULL);
142 : 6 : (*add_advisor_fn) (pgsa_advisor);
143 : 6 : }
144 : :
145 : : /*
146 : : * Get the advice string that has been configured for this query, if any,
147 : : * and return it. Otherwise, return NULL.
148 : : */
149 : : static char *
150 : 53 : pgsa_advisor(PlannerGlobal *glob, Query *parse,
151 : : const char *query_string, int cursorOptions,
152 : : ExplainState *es)
153 : : {
154 : : pgsa_entry_key key;
155 : : pgsa_entry *entry;
156 : : char *advice_string;
157 : : uint64 stash_id;
158 : :
159 : : /*
160 : : * Exit quickly if the stash name is empty or there's no query ID.
161 : : */
162 [ + + - + ]: 53 : if (pg_stash_advice_stash_name[0] == '\0' || parse->queryId == 0)
163 : 23 : return NULL;
164 : :
165 : : /* Attach to dynamic shared memory if not already done. */
166 [ - + ]: 30 : if (unlikely(pgsa_entry_dshash == NULL))
29 rhaas@postgresql.org 167 :UNC 0 : pgsa_attach();
168 : :
169 : : /* If stash data is still being restored from disk, ignore. */
28 rhaas@postgresql.org 170 [ - + ]:GNC 30 : if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready))
28 rhaas@postgresql.org 171 :UNC 0 : return NULL;
172 : :
173 : : /*
174 : : * Translate pg_stash_advice.stash_name to an integer ID.
175 : : *
176 : : * pgsa_check_stash_name_guc() has already validated the advice stash
177 : : * name, so we don't need to call pgsa_check_stash_name() here.
178 : : */
29 rhaas@postgresql.org 179 :GNC 30 : stash_id = pgsa_lookup_stash_id(pg_stash_advice_stash_name);
180 [ + + ]: 30 : if (stash_id == 0)
181 : 1 : return NULL;
182 : :
183 : : /*
184 : : * Look up the advice string for the given stash ID + query ID.
185 : : *
186 : : * If we find an advice string, we copy it into the current memory
187 : : * context, presumably short-lived, so that we can release the lock on the
188 : : * dshash entry. pg_plan_advice only needs the value to remain allocated
189 : : * long enough for it to be parsed, so this should be good enough.
190 : : */
191 : 29 : memset(&key, 0, sizeof(pgsa_entry_key));
192 : 29 : key.pgsa_stash_id = stash_id;
193 : 29 : key.queryId = parse->queryId;
194 : 29 : entry = dshash_find(pgsa_entry_dshash, &key, false);
195 [ + + ]: 29 : if (entry == NULL)
196 : 25 : return NULL;
197 [ - + ]: 4 : if (entry->advice_string == InvalidDsaPointer)
29 rhaas@postgresql.org 198 :UNC 0 : advice_string = NULL;
199 : : else
29 rhaas@postgresql.org 200 :GNC 4 : advice_string = pstrdup(dsa_get_address(pgsa_dsa_area,
201 : : entry->advice_string));
202 : 4 : dshash_release_lock(pgsa_entry_dshash, entry);
203 : :
204 : : /* If we found an advice string, emit a debug message. */
205 [ + - ]: 4 : if (advice_string != NULL)
206 [ - + ]: 4 : elog(DEBUG2, "supplying automatic advice for stash \"%s\", query ID %" PRId64 ": %s",
207 : : pg_stash_advice_stash_name, key.queryId, advice_string);
208 : :
209 : 4 : return advice_string;
210 : : }
211 : :
212 : : /*
213 : : * Attach to various structures in dynamic shared memory.
214 : : *
215 : : * This function is designed to be resilient against errors. That is, if it
216 : : * fails partway through, it should be possible to call it again, repeat no
217 : : * work already completed, and potentially succeed or at least get further if
218 : : * whatever caused the previous failure has been corrected.
219 : : */
220 : : void
221 : 13 : pgsa_attach(void)
222 : : {
223 : : bool found;
224 : : MemoryContext oldcontext;
225 : :
226 : : /*
227 : : * Create a memory context to make sure that any control structures
228 : : * allocated in local memory are sufficiently persistent.
229 : : */
230 [ + - ]: 13 : if (pg_stash_advice_mcxt == NULL)
231 : 13 : pg_stash_advice_mcxt = AllocSetContextCreate(TopMemoryContext,
232 : : "pg_stash_advice",
233 : : ALLOCSET_DEFAULT_SIZES);
234 : 13 : oldcontext = MemoryContextSwitchTo(pg_stash_advice_mcxt);
235 : :
236 : : /* Attach to the fixed-size state object if not already done. */
237 [ + - ]: 13 : if (pgsa_state == NULL)
238 : 13 : pgsa_state = GetNamedDSMSegment("pg_stash_advice",
239 : : sizeof(pgsa_shared_state),
240 : : pgsa_init_shared_state,
241 : : &found, NULL);
242 : :
243 : : /* Attach to the DSA area if not already done. */
244 [ + - ]: 13 : if (pgsa_dsa_area == NULL)
245 : : {
246 : : dsa_handle area_handle;
247 : :
248 : 13 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
249 : 13 : area_handle = pgsa_state->area;
250 [ + + ]: 13 : if (area_handle == DSA_HANDLE_INVALID)
251 : : {
252 : 5 : pgsa_dsa_area = dsa_create(pgsa_state->dsa_tranche);
253 : 5 : dsa_pin(pgsa_dsa_area);
254 : 5 : pgsa_state->area = dsa_get_handle(pgsa_dsa_area);
255 : 5 : LWLockRelease(&pgsa_state->lock);
256 : : }
257 : : else
258 : : {
259 : 8 : LWLockRelease(&pgsa_state->lock);
260 : 8 : pgsa_dsa_area = dsa_attach(area_handle);
261 : : }
262 : 13 : dsa_pin_mapping(pgsa_dsa_area);
263 : : }
264 : :
265 : : /* Attach to the stash_name->stash_id hash table if not already done. */
266 [ + - ]: 13 : if (pgsa_stash_dshash == NULL)
267 : : {
268 : : dshash_table_handle stash_handle;
269 : :
270 : 13 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
271 : 13 : pgsa_stash_dshash_parameters.tranche_id = pgsa_state->stash_tranche;
272 : 13 : stash_handle = pgsa_state->stash_hash;
273 [ + + ]: 13 : if (stash_handle == DSHASH_HANDLE_INVALID)
274 : : {
275 : 5 : pgsa_stash_dshash = dshash_create(pgsa_dsa_area,
276 : : &pgsa_stash_dshash_parameters,
277 : : NULL);
278 : 10 : pgsa_state->stash_hash =
279 : 5 : dshash_get_hash_table_handle(pgsa_stash_dshash);
280 : 5 : LWLockRelease(&pgsa_state->lock);
281 : : }
282 : : else
283 : : {
284 : 8 : LWLockRelease(&pgsa_state->lock);
285 : 8 : pgsa_stash_dshash = dshash_attach(pgsa_dsa_area,
286 : : &pgsa_stash_dshash_parameters,
287 : : stash_handle, NULL);
288 : : }
289 : : }
290 : :
291 : : /* Attach to the entry hash table if not already done. */
292 [ + - ]: 13 : if (pgsa_entry_dshash == NULL)
293 : : {
294 : : dshash_table_handle entry_handle;
295 : :
296 : 13 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
297 : 13 : pgsa_entry_dshash_parameters.tranche_id = pgsa_state->entry_tranche;
298 : 13 : entry_handle = pgsa_state->entry_hash;
299 [ + + ]: 13 : if (entry_handle == DSHASH_HANDLE_INVALID)
300 : : {
301 : 5 : pgsa_entry_dshash = dshash_create(pgsa_dsa_area,
302 : : &pgsa_entry_dshash_parameters,
303 : : NULL);
304 : 10 : pgsa_state->entry_hash =
305 : 5 : dshash_get_hash_table_handle(pgsa_entry_dshash);
306 : 5 : LWLockRelease(&pgsa_state->lock);
307 : : }
308 : : else
309 : : {
310 : 8 : LWLockRelease(&pgsa_state->lock);
311 : 8 : pgsa_entry_dshash = dshash_attach(pgsa_dsa_area,
312 : : &pgsa_entry_dshash_parameters,
313 : : entry_handle, NULL);
314 : : }
315 : : }
316 : :
317 : : /* Restore previous memory context. */
318 : 13 : MemoryContextSwitchTo(oldcontext);
319 : 13 : }
320 : :
321 : : /*
322 : : * Error out if the stashes have not been loaded from disk yet.
323 : : */
324 : : void
28 325 : 22 : pgsa_check_lockout(void)
326 : : {
327 [ - + ]: 22 : if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready))
28 rhaas@postgresql.org 328 [ # # ]:UNC 0 : ereport(ERROR,
329 : : (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
330 : : errmsg("stash modifications are not allowed because \"%s\" has not been loaded yet",
331 : : PGSA_DUMP_FILE)));
28 rhaas@postgresql.org 332 :GNC 22 : }
333 : :
334 : : /*
335 : : * Check whether an advice stash name is legal, and signal an error if not.
336 : : *
337 : : * Keep this in sync with pgsa_check_stash_name_guc, below.
338 : : */
339 : : void
29 340 : 31 : pgsa_check_stash_name(char *stash_name)
341 : : {
342 : : /* Reject empty advice stash name. */
343 [ + + ]: 31 : if (stash_name[0] == '\0')
344 [ + - ]: 1 : ereport(ERROR,
345 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
346 : : errmsg("advice stash name may not be zero length"));
347 : :
348 : : /* Reject overlong advice stash names. */
349 [ + + ]: 30 : if (strlen(stash_name) + 1 > NAMEDATALEN)
350 [ + - ]: 1 : ereport(ERROR,
351 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
352 : : errmsg("advice stash names may not be longer than %d bytes",
353 : : NAMEDATALEN - 1));
354 : :
355 : : /*
356 : : * Reject non-ASCII advice stash names, since advice stashes are visible
357 : : * across all databases and the encodings of those databases might differ.
358 : : */
359 [ + + ]: 29 : if (!pg_is_ascii(stash_name))
360 [ + - ]: 1 : ereport(ERROR,
361 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
362 : : errmsg("advice stash name must not contain non-ASCII characters"));
363 : :
364 : : /*
365 : : * Reject things that do not look like identifiers, since the ability to
366 : : * create an advice stash with non-printable characters or weird symbols
367 : : * in the name is not likely to be useful to anyone.
368 : : */
369 [ + + ]: 28 : if (!pgsa_is_identifier(stash_name))
370 [ + - ]: 1 : ereport(ERROR,
371 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
372 : : errmsg("advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores"));
373 : 27 : }
374 : :
375 : : /*
376 : : * As above, but for the GUC check_hook. We allow the empty string here,
377 : : * though, as equivalent to disabling the feature.
378 : : */
379 : : static bool
380 : 11 : pgsa_check_stash_name_guc(char **newval, void **extra, GucSource source)
381 : : {
382 : 11 : char *stash_name = *newval;
383 : :
384 : : /* Reject overlong advice stash names. */
385 [ - + ]: 11 : if (strlen(stash_name) + 1 > NAMEDATALEN)
386 : : {
29 rhaas@postgresql.org 387 :UNC 0 : GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
388 : 0 : GUC_check_errdetail("advice stash names may not be longer than %d bytes",
389 : : NAMEDATALEN - 1);
390 : 0 : return false;
391 : : }
392 : :
393 : : /*
394 : : * Reject non-ASCII advice stash names, since advice stashes are visible
395 : : * across all databases and the encodings of those databases might differ.
396 : : */
29 rhaas@postgresql.org 397 [ + + ]:GNC 11 : if (!pg_is_ascii(stash_name))
398 : : {
399 : 1 : GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
400 : 1 : GUC_check_errdetail("advice stash name must not contain non-ASCII characters");
401 : 1 : return false;
402 : : }
403 : :
404 : : /*
405 : : * Reject things that do not look like identifiers, since the ability to
406 : : * create an advice stash with non-printable characters or weird symbols
407 : : * in the name is not likely to be useful to anyone.
408 : : */
409 [ + + ]: 10 : if (!pgsa_is_identifier(stash_name))
410 : : {
411 : 1 : GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
412 : 1 : GUC_check_errdetail("advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores");
413 : 1 : return false;
414 : : }
415 : :
416 : 9 : return true;
417 : : }
418 : :
419 : : /*
420 : : * Create an advice stash.
421 : : */
422 : : void
423 : 11 : pgsa_create_stash(char *stash_name)
424 : : {
425 : : pgsa_stash *stash;
426 : : bool found;
427 : :
428 [ - + ]: 11 : Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE));
429 : :
430 : : /* Create a stash with this name, unless one already exists. */
431 : 11 : stash = dshash_find_or_insert(pgsa_stash_dshash, stash_name, &found);
432 [ + + ]: 11 : if (found)
433 [ + - ]: 1 : ereport(ERROR,
434 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
435 : : errmsg("advice stash \"%s\" already exists", stash_name));
436 : 10 : stash->pgsa_stash_id = pgsa_state->next_stash_id++;
437 : 10 : dshash_release_lock(pgsa_stash_dshash, stash);
438 : :
439 : : /* Bump change count. */
28 440 : 10 : pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1);
29 441 : 10 : }
442 : :
443 : : /*
444 : : * Remove any stored advice string for the given advice stash and query ID.
445 : : */
446 : : void
447 : 2 : pgsa_clear_advice_string(char *stash_name, int64 queryId)
448 : : {
449 : : pgsa_entry *entry;
450 : : pgsa_entry_key key;
451 : : uint64 stash_id;
452 : : dsa_pointer old_dp;
453 : :
454 [ - + ]: 2 : Assert(LWLockHeldByMe(&pgsa_state->lock));
455 : :
456 : : /* Translate the stash name to an integer ID. */
457 [ + + ]: 2 : if ((stash_id = pgsa_lookup_stash_id(stash_name)) == 0)
458 [ + - ]: 1 : ereport(ERROR,
459 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
460 : : errmsg("advice stash \"%s\" does not exist", stash_name));
461 : :
462 : : /*
463 : : * Look for an existing entry, and free it. But, be sure to save the
464 : : * pointer to the associated advice string, if any.
465 : : */
466 : 1 : memset(&key, 0, sizeof(pgsa_entry_key));
467 : 1 : key.pgsa_stash_id = stash_id;
468 : 1 : key.queryId = queryId;
469 : 1 : entry = dshash_find(pgsa_entry_dshash, &key, true);
470 [ - + ]: 1 : if (entry == NULL)
29 rhaas@postgresql.org 471 :UNC 0 : old_dp = InvalidDsaPointer;
472 : : else
473 : : {
29 rhaas@postgresql.org 474 :GNC 1 : old_dp = entry->advice_string;
475 : 1 : dshash_delete_entry(pgsa_entry_dshash, entry);
476 : : }
477 : :
478 : : /* Now we free the advice string as well, if there was one. */
479 [ + - ]: 1 : if (old_dp != InvalidDsaPointer)
480 : 1 : dsa_free(pgsa_dsa_area, old_dp);
481 : :
482 : : /* Bump change count. */
28 483 : 1 : pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1);
29 484 : 1 : }
485 : :
486 : : /*
487 : : * Drop an advice stash.
488 : : */
489 : : void
490 : 6 : pgsa_drop_stash(char *stash_name)
491 : : {
492 : : pgsa_entry *entry;
493 : : pgsa_stash *stash;
494 : : dshash_seq_status iterator;
495 : : uint64 stash_id;
496 : :
497 [ - + ]: 6 : Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE));
498 : :
499 : : /* Remove the entry for this advice stash. */
500 : 6 : stash = dshash_find(pgsa_stash_dshash, stash_name, true);
501 [ + + ]: 6 : if (stash == NULL)
502 [ + - ]: 1 : ereport(ERROR,
503 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
504 : : errmsg("advice stash \"%s\" does not exist", stash_name));
505 : 5 : stash_id = stash->pgsa_stash_id;
506 : 5 : dshash_delete_entry(pgsa_stash_dshash, stash);
507 : :
508 : : /*
509 : : * Now remove all the entries. Since pgsa_state->lock must be held at
510 : : * least in shared mode to insert entries into pgsa_entry_dshash, it
511 : : * doesn't matter whether we do this before or after deleting the entry
512 : : * from pgsa_stash_dshash.
513 : : */
514 : 5 : dshash_seq_init(&iterator, pgsa_entry_dshash, true);
515 [ + + ]: 15 : while ((entry = dshash_seq_next(&iterator)) != NULL)
516 : : {
517 [ + + ]: 5 : if (stash_id == entry->key.pgsa_stash_id)
518 : : {
519 [ + - ]: 4 : if (entry->advice_string != InvalidDsaPointer)
520 : 4 : dsa_free(pgsa_dsa_area, entry->advice_string);
521 : 4 : dshash_delete_current(&iterator);
522 : : }
523 : : }
524 : 5 : dshash_seq_term(&iterator);
525 : :
526 : : /* Bump change count. */
28 527 : 5 : pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1);
528 : 5 : }
529 : :
530 : : /*
531 : : * Remove all stashes and entries from shared memory.
532 : : *
533 : : * This is intended to be called before reloading from a dump file, so that
534 : : * a failed previous attempt doesn't leave stale data behind.
535 : : */
536 : : void
537 : 4 : pgsa_reset_all_stashes(void)
538 : : {
539 : : dshash_seq_status iter;
540 : : pgsa_entry *entry;
541 : :
542 [ - + ]: 4 : Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE));
543 : :
544 : : /* Remove all stashes. */
545 : 4 : dshash_seq_init(&iter, pgsa_stash_dshash, true);
546 [ - + ]: 4 : while (dshash_seq_next(&iter) != NULL)
28 rhaas@postgresql.org 547 :UNC 0 : dshash_delete_current(&iter);
28 rhaas@postgresql.org 548 :GNC 4 : dshash_seq_term(&iter);
549 : :
550 : : /* Remove all entries. */
551 : 4 : dshash_seq_init(&iter, pgsa_entry_dshash, true);
552 [ - + ]: 4 : while ((entry = dshash_seq_next(&iter)) != NULL)
553 : : {
28 rhaas@postgresql.org 554 [ # # ]:UNC 0 : if (entry->advice_string != InvalidDsaPointer)
555 : 0 : dsa_free(pgsa_dsa_area, entry->advice_string);
556 : 0 : dshash_delete_current(&iter);
557 : : }
28 rhaas@postgresql.org 558 :GNC 4 : dshash_seq_term(&iter);
559 : :
560 : : /* Reset the stash ID counter. */
561 : 4 : pgsa_state->next_stash_id = UINT64CONST(1);
29 562 : 4 : }
563 : :
564 : : /*
565 : : * Initialize shared state when first created.
566 : : */
567 : : static void
568 : 5 : pgsa_init_shared_state(void *ptr, void *arg)
569 : : {
570 : 5 : pgsa_shared_state *state = (pgsa_shared_state *) ptr;
571 : :
572 : 5 : LWLockInitialize(&state->lock,
573 : : LWLockNewTrancheId("pg_stash_advice_lock"));
574 : 5 : state->dsa_tranche = LWLockNewTrancheId("pg_stash_advice_dsa");
575 : 5 : state->stash_tranche = LWLockNewTrancheId("pg_stash_advice_stash");
576 : 5 : state->entry_tranche = LWLockNewTrancheId("pg_stash_advice_entry");
577 : 5 : state->next_stash_id = UINT64CONST(1);
578 : 5 : state->area = DSA_HANDLE_INVALID;
579 : 5 : state->stash_hash = DSHASH_HANDLE_INVALID;
580 : 5 : state->entry_hash = DSHASH_HANDLE_INVALID;
28 581 : 5 : state->bgworker_pid = InvalidPid;
582 : 5 : pg_atomic_init_flag(&state->stashes_ready);
583 : 5 : pg_atomic_init_u64(&state->change_count, 0);
584 : :
585 : : /*
586 : : * If this module was loaded via shared_preload_libraries, then
587 : : * pg_stash_advice_persist is a GUC variable. If it's true, that means
588 : : * that we should lock out manual stash modifications until the dump file
589 : : * has been successfully loaded. If it's false, there's nothing to load,
590 : : * so we set stashes_ready immediately.
591 : : *
592 : : * If this module was not loaded via shared_preload_libraries, then
593 : : * pg_stash_advice_persist is not a GUC variable, but it will be false,
594 : : * which leads to the correct behavior.
595 : : */
596 [ + + ]: 5 : if (!pg_stash_advice_persist)
597 : 1 : pg_atomic_test_set_flag(&state->stashes_ready);
29 598 : 5 : }
599 : :
600 : : /*
601 : : * Check whether a string looks like a valid identifier. It must contain only
602 : : * ASCII identifier characters, and must not begin with a digit.
603 : : */
604 : : static bool
605 : 38 : pgsa_is_identifier(char *str)
606 : : {
607 [ + + + + ]: 38 : if (*str >= '0' && *str <= '9')
608 : 1 : return false;
609 : :
610 [ + + ]: 397 : while (*str != '\0')
611 : : {
612 : 361 : char c = *str++;
613 : :
614 [ + + + - : 361 : if ((c < '0' || c > '9') && (c < 'a' || c > 'z') &&
+ + - + +
+ ]
615 [ + - + + ]: 39 : (c < 'A' || c > 'Z') && c != '_')
616 : 1 : return false;
617 : : }
618 : :
619 : 36 : return true;
620 : : }
621 : :
622 : : /*
623 : : * Look up the integer ID that corresponds to the given stash name.
624 : : *
625 : : * Returns 0 if no such stash exists.
626 : : */
627 : : uint64
628 : 50 : pgsa_lookup_stash_id(char *stash_name)
629 : : {
630 : : pgsa_stash *stash;
631 : : uint64 stash_id;
632 : :
633 : : /* Search the shared hash table. */
634 : 50 : stash = dshash_find(pgsa_stash_dshash, stash_name, false);
635 [ + + ]: 50 : if (stash == NULL)
636 : 4 : return 0;
637 : 46 : stash_id = stash->pgsa_stash_id;
638 : 46 : dshash_release_lock(pgsa_stash_dshash, stash);
639 : :
640 : 46 : return stash_id;
641 : : }
642 : :
643 : : /*
644 : : * Store a new or updated advice string for the given advice stash and query ID.
645 : : */
646 : : void
647 : 14 : pgsa_set_advice_string(char *stash_name, int64 queryId, char *advice_string)
648 : : {
649 : : pgsa_entry *entry;
650 : : bool found;
651 : : pgsa_entry_key key;
652 : : uint64 stash_id;
653 : : dsa_pointer new_dp;
654 : : dsa_pointer old_dp;
655 : :
656 : : /*
657 : : * The caller must hold our lock, at least in shared mode. This is
658 : : * important for two reasons.
659 : : *
660 : : * First, it holds off interrupts, so that we can't bail out of this code
661 : : * after allocating DSA memory for the advice string and before storing
662 : : * the resulting pointer somewhere that others can find it.
663 : : *
664 : : * Second, we need to avoid a race against pgsa_drop_stash(). That
665 : : * function removes a stash_name->stash_id mapping and all the entries for
666 : : * that stash_id. Without the lock, there's a race condition no matter
667 : : * which of those things it does first, because as soon as we've looked up
668 : : * the stash ID, that whole function can execute before we do the rest of
669 : : * our work, which would result in us adding an entry for a stash that no
670 : : * longer exists.
671 : : */
672 [ - + ]: 14 : Assert(LWLockHeldByMe(&pgsa_state->lock));
673 : :
674 : : /* Look up the stash ID. */
675 [ + + ]: 14 : if ((stash_id = pgsa_lookup_stash_id(stash_name)) == 0)
676 [ + - ]: 1 : ereport(ERROR,
677 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
678 : : errmsg("advice stash \"%s\" does not exist", stash_name));
679 : :
680 : : /* Allocate space for the advice string. */
681 : 13 : new_dp = dsa_allocate(pgsa_dsa_area, strlen(advice_string) + 1);
682 : 13 : strcpy(dsa_get_address(pgsa_dsa_area, new_dp), advice_string);
683 : :
684 : : /* Attempt to insert an entry into the hash table. */
685 : 13 : memset(&key, 0, sizeof(pgsa_entry_key));
686 : 13 : key.pgsa_stash_id = stash_id;
687 : 13 : key.queryId = queryId;
688 : 13 : entry = dshash_find_or_insert_extended(pgsa_entry_dshash, &key, &found,
689 : : DSHASH_INSERT_NO_OOM);
690 : :
691 : : /*
692 : : * If it didn't work, bail out, being careful to free the shared memory
693 : : * we've already allocated before, since error cleanup will not do so.
694 : : */
695 [ - + ]: 13 : if (entry == NULL)
696 : : {
29 rhaas@postgresql.org 697 :UNC 0 : dsa_free(pgsa_dsa_area, new_dp);
698 [ # # ]: 0 : ereport(ERROR,
699 : : errcode(ERRCODE_OUT_OF_MEMORY),
700 : : errmsg("out of memory"),
701 : : errdetail("could not insert advice string into shared hash table"));
702 : : }
703 : :
704 : : /* Update the entry and release the lock. */
29 rhaas@postgresql.org 705 [ + + ]:GNC 13 : old_dp = found ? entry->advice_string : InvalidDsaPointer;
706 : 13 : entry->advice_string = new_dp;
707 : 13 : dshash_release_lock(pgsa_entry_dshash, entry);
708 : :
709 : : /*
710 : : * We're not safe from leaks yet!
711 : : *
712 : : * There's now a pointer to new_dp in the entry that we just updated, but
713 : : * that means that there's no longer anything pointing to old_dp.
714 : : */
715 [ + + ]: 13 : if (DsaPointerIsValid(old_dp))
716 : 2 : dsa_free(pgsa_dsa_area, old_dp);
717 : :
718 : : /* Bump change count. */
28 719 : 13 : pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1);
720 : 13 : }
721 : :
722 : : /*
723 : : * Start our worker process.
724 : : */
725 : : void
726 : 4 : pgsa_start_worker(void)
727 : : {
728 : 4 : BackgroundWorker worker = {0};
729 : : BackgroundWorkerHandle *handle;
730 : : BgwHandleStatus status;
731 : : pid_t pid;
732 : :
733 : 4 : worker.bgw_flags = BGWORKER_SHMEM_ACCESS;
734 : 4 : worker.bgw_start_time = BgWorkerStart_ConsistentState;
735 : 4 : worker.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL;
736 : 4 : strcpy(worker.bgw_library_name, "pg_stash_advice");
737 : 4 : strcpy(worker.bgw_function_name, "pg_stash_advice_worker_main");
738 : 4 : strcpy(worker.bgw_name, "pg_stash_advice worker");
739 : 4 : strcpy(worker.bgw_type, "pg_stash_advice worker");
740 : :
741 : : /*
742 : : * If process_shared_preload_libraries_in_progress = true, we may be in
743 : : * the postmaster, in which case this will really register the worker, or
744 : : * we may be in a child process in an EXEC_BACKEND build, in which case it
745 : : * will silently do nothing (which is the correct behavior).
746 : : */
747 [ + - ]: 4 : if (process_shared_preload_libraries_in_progress)
748 : : {
749 : 4 : RegisterBackgroundWorker(&worker);
750 : 4 : return;
751 : : }
752 : :
753 : : /*
754 : : * If process_shared_preload_libraries_in_progress = false, we're being
755 : : * asked to start the worker after system startup time. In other words,
756 : : * unless this is single-user mode, we're not in the postmaster, so we
757 : : * should use RegisterDynamicBackgroundWorker and then wait for startup to
758 : : * complete. (If we do happen to be in single-user mode, this will error
759 : : * out, which is fine.)
760 : : */
28 rhaas@postgresql.org 761 :UNC 0 : worker.bgw_notify_pid = MyProcPid;
762 [ # # ]: 0 : if (!RegisterDynamicBackgroundWorker(&worker, &handle))
763 [ # # ]: 0 : ereport(ERROR,
764 : : (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
765 : : errmsg("could not register background process"),
766 : : errhint("You may need to increase \"max_worker_processes\".")));
767 : 0 : status = WaitForBackgroundWorkerStartup(handle, &pid);
768 [ # # ]: 0 : if (status != BGWH_STARTED)
769 [ # # ]: 0 : ereport(ERROR,
770 : : (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
771 : : errmsg("could not start background process"),
772 : : errhint("More details may be available in the server log.")));
773 : : }
|