Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * stashpersist.c
4 : * Persistence support for pg_stash_advice.
5 : *
6 : * Copyright (c) 2016-2026, PostgreSQL Global Development Group
7 : *
8 : * contrib/pg_stash_advice/stashpersist.c
9 : *
10 : *-------------------------------------------------------------------------
11 : */
12 : #include "postgres.h"
13 :
14 : #include <sys/stat.h>
15 :
16 : #include "common/hashfn.h"
17 : #include "miscadmin.h"
18 : #include "pg_stash_advice.h"
19 : #include "postmaster/bgworker.h"
20 : #include "postmaster/interrupt.h"
21 : #include "storage/fd.h"
22 : #include "storage/ipc.h"
23 : #include "storage/latch.h"
24 : #include "storage/proc.h"
25 : #include "storage/procsignal.h"
26 : #include "utils/backend_status.h"
27 : #include "utils/guc.h"
28 : #include "utils/memutils.h"
29 : #include "utils/timestamp.h"
30 :
31 : typedef struct pgsa_writer_context
32 : {
33 : char pathname[MAXPGPATH];
34 : FILE *file;
35 : pgsa_stash_name_table_hash *nhash;
36 : StringInfoData buf;
37 : int entries_written;
38 : } pgsa_writer_context;
39 :
40 : /*
41 : * A parsed entry line, with pointers into the slurp buffer.
42 : */
43 : typedef struct pgsa_saved_entry
44 : {
45 : char *stash_name;
46 : int64 queryId;
47 : char *advice_string;
48 : } pgsa_saved_entry;
49 :
50 : /*
51 : * simplehash for detecting duplicate stash names during parsing.
52 : * Keyed by stash name (char *), pointing into the slurp buffer.
53 : */
54 : typedef struct pgsa_saved_stash
55 : {
56 : uint32 status;
57 : char *name;
58 : } pgsa_saved_stash;
59 :
60 : #define SH_PREFIX pgsa_saved_stash_table
61 : #define SH_ELEMENT_TYPE pgsa_saved_stash
62 : #define SH_KEY_TYPE char *
63 : #define SH_KEY name
64 : #define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) (key), strlen(key))
65 : #define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
66 : #define SH_SCOPE static inline
67 : #define SH_DEFINE
68 : #define SH_DECLARE
69 : #include "lib/simplehash.h"
70 :
71 : extern PGDLLEXPORT void pg_stash_advice_worker_main(Datum main_arg);
72 : static void pgsa_append_tsv_escaped_string(StringInfo buf, const char *str);
73 : static void pgsa_detach_shmem(int code, Datum arg);
74 : static char *pgsa_next_tsv_field(char **cursor);
75 : static void pgsa_read_from_disk(void);
76 : static void pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries);
77 : static void pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes);
78 : static void pgsa_unescape_tsv_field(char *str, const char *filename,
79 : unsigned lineno);
80 : static void pgsa_write_entries(pgsa_writer_context *wctx);
81 : pg_noreturn static void pgsa_write_error(pgsa_writer_context *wctx);
82 : static void pgsa_write_stashes(pgsa_writer_context *wctx);
83 : static void pgsa_write_to_disk(void);
84 :
85 : /*
86 : * Background worker entry point for pg_stash_advice persistence.
87 : *
88 : * On startup, if stashes_ready is set, we load previously saved
89 : * stash data from disk. Then we enter a loop, periodically checking whether
90 : * any changes have been made (via the change_count atomic counter) and
91 : * writing them to disk. On shutdown, we perform a final write.
92 : */
93 : PGDLLEXPORT void
94 4 : pg_stash_advice_worker_main(Datum main_arg)
95 : {
96 : uint64 last_change_count;
97 4 : TimestampTz last_write_time = 0;
98 :
99 : /* Establish signal handlers; once that's done, unblock signals. */
100 4 : pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
101 4 : pqsignal(SIGHUP, SignalHandlerForConfigReload);
102 4 : pqsignal(SIGUSR1, procsignal_sigusr1_handler);
103 4 : BackgroundWorkerUnblockSignals();
104 :
105 : /* Log a debug message */
106 4 : ereport(DEBUG1,
107 : errmsg("pg_stash_advice worker started"));
108 :
109 : /* Set up session user so pgstat can report it. */
110 4 : InitializeSessionUserIdStandalone();
111 :
112 : /* Report this worker in pg_stat_activity. */
113 4 : pgstat_beinit();
114 4 : pgstat_bestart_initial();
115 4 : pgstat_bestart_final();
116 :
117 : /* Attach to shared memory structures. */
118 4 : pgsa_attach();
119 :
120 : /* Set on-detach hook so that our PID will be cleared on exit. */
121 4 : before_shmem_exit(pgsa_detach_shmem, 0);
122 :
123 : /*
124 : * Store our PID in shared memory, unless there's already another worker
125 : * running, in which case just exit.
126 : */
127 4 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
128 4 : if (pgsa_state->bgworker_pid != InvalidPid)
129 : {
130 0 : LWLockRelease(&pgsa_state->lock);
131 0 : ereport(LOG,
132 : (errmsg("pg_stash_advice worker is already running under PID %d",
133 : (int) pgsa_state->bgworker_pid)));
134 0 : return;
135 : }
136 4 : pgsa_state->bgworker_pid = MyProcPid;
137 4 : LWLockRelease(&pgsa_state->lock);
138 :
139 : /*
140 : * If pg_stash_advice.persist was set to true during
141 : * process_shared_preload_libraries() and the data has not yet been
142 : * successfully loaded, load it now.
143 : */
144 4 : if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready))
145 : {
146 4 : pgsa_read_from_disk();
147 4 : pg_atomic_test_set_flag(&pgsa_state->stashes_ready);
148 : }
149 :
150 : /* Note the current change count so we can detect future changes. */
151 4 : last_change_count = pg_atomic_read_u64(&pgsa_state->change_count);
152 :
153 : /* Periodically write to disk until terminated. */
154 12 : while (!ShutdownRequestPending)
155 : {
156 : /* In case of a SIGHUP, just reload the configuration. */
157 8 : if (ConfigReloadPending)
158 : {
159 0 : ConfigReloadPending = false;
160 0 : ProcessConfigFile(PGC_SIGHUP);
161 : }
162 :
163 8 : if (pg_stash_advice_persist_interval <= 0)
164 : {
165 : /* Only writing at shutdown, so just wait forever. */
166 8 : (void) WaitLatch(MyLatch,
167 : WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
168 : -1L,
169 : PG_WAIT_EXTENSION);
170 : }
171 : else
172 : {
173 : TimestampTz next_write_time;
174 : long delay_in_ms;
175 : uint64 current_change_count;
176 :
177 : /* Compute when the next write should happen. */
178 0 : next_write_time =
179 0 : TimestampTzPlusMilliseconds(last_write_time,
180 : pg_stash_advice_persist_interval * 1000);
181 : delay_in_ms =
182 0 : TimestampDifferenceMilliseconds(GetCurrentTimestamp(),
183 : next_write_time);
184 :
185 : /*
186 : * When we reach next_write_time, we always update last_write_time
187 : * (which is really the time at which we last considered writing),
188 : * but we only actually write to disk if something has changed.
189 : */
190 0 : if (delay_in_ms <= 0)
191 : {
192 : current_change_count =
193 0 : pg_atomic_read_u64(&pgsa_state->change_count);
194 0 : if (current_change_count != last_change_count)
195 : {
196 0 : pgsa_write_to_disk();
197 0 : last_change_count = current_change_count;
198 : }
199 0 : last_write_time = GetCurrentTimestamp();
200 0 : continue;
201 : }
202 :
203 : /* Sleep until the next write time. */
204 0 : (void) WaitLatch(MyLatch,
205 : WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
206 : delay_in_ms,
207 : PG_WAIT_EXTENSION);
208 : }
209 :
210 8 : ResetLatch(MyLatch);
211 : }
212 :
213 : /* Write one last time before exiting. */
214 4 : pgsa_write_to_disk();
215 : }
216 :
217 : /*
218 : * Clear our PID from shared memory on exit.
219 : */
220 : static void
221 4 : pgsa_detach_shmem(int code, Datum arg)
222 : {
223 4 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
224 4 : if (pgsa_state->bgworker_pid == MyProcPid)
225 4 : pgsa_state->bgworker_pid = InvalidPid;
226 4 : LWLockRelease(&pgsa_state->lock);
227 4 : }
228 :
229 : /*
230 : * Load advice stash data from a dump file on disk, if there is one.
231 : */
232 : static void
233 4 : pgsa_read_from_disk(void)
234 : {
235 : struct stat statbuf;
236 : FILE *file;
237 : char *filebuf;
238 : size_t nread;
239 : char *p;
240 : unsigned lineno;
241 : pgsa_saved_stash_table_hash *saved_stashes;
242 4 : int num_stashes = 0;
243 : pgsa_saved_entry *entries;
244 4 : int num_entries = 0;
245 4 : int max_entries = 64;
246 : MemoryContext tmpcxt;
247 : MemoryContext oldcxt;
248 :
249 : Assert(pgsa_entry_dshash != NULL);
250 :
251 : /*
252 : * Clear any existing shared memory state.
253 : *
254 : * Normally, there won't be any, but if this function was called before
255 : * and failed after beginning to apply changes to shared memory, then we
256 : * need to get rid of any entries created at that time before trying
257 : * again.
258 : */
259 4 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
260 4 : pgsa_reset_all_stashes();
261 4 : LWLockRelease(&pgsa_state->lock);
262 :
263 : /* Open the dump file. If it doesn't exist, we're done. */
264 4 : file = AllocateFile(PGSA_DUMP_FILE, "r");
265 4 : if (!file)
266 : {
267 2 : if (errno == ENOENT)
268 2 : return;
269 0 : ereport(ERROR,
270 : (errcode_for_file_access(),
271 : errmsg("could not open file \"%s\": %m", PGSA_DUMP_FILE)));
272 : }
273 :
274 : /* Use a temporary context for all parse-phase allocations. */
275 2 : tmpcxt = AllocSetContextCreate(CurrentMemoryContext,
276 : "pg_stash_advice load",
277 : ALLOCSET_DEFAULT_SIZES);
278 2 : oldcxt = MemoryContextSwitchTo(tmpcxt);
279 :
280 : /* Figure out how long the file is. */
281 2 : if (fstat(fileno(file), &statbuf) != 0)
282 0 : ereport(ERROR,
283 : (errcode_for_file_access(),
284 : errmsg("could not stat file \"%s\": %m", PGSA_DUMP_FILE)));
285 :
286 : /*
287 : * Slurp the entire file into memory all at once.
288 : *
289 : * We could avoid this by reading the file incrementally and applying
290 : * changes to pgsa_stash_dshash and pgsa_entry_dshash as we go. Given the
291 : * lockout mechanism implemented by stashes_ready, that shouldn't have any
292 : * user-visible behavioral consequences, but it would consume shared
293 : * memory to no benefit. It seems better to buffer everything in private
294 : * memory first, and then only apply the changes once the file has been
295 : * successfully parsed in its entirety.
296 : *
297 : * That also has the advantage of possibly being more future-proof: if we
298 : * decide to remove the stashes_ready mechanism in the future, or say
299 : * allow for multiple save files, fully validating the file before
300 : * applying any changes will become much more important.
301 : *
302 : * Of course, this approach does have one major disadvantage, which is
303 : * that we'll temporarily use about twice as much memory as we're
304 : * ultimately going to need, but that seems like it shouldn't be a problem
305 : * in practice. If there's so much stashed advice that parsing the disk
306 : * file runs us out of memory, something has gone terribly wrong. In that
307 : * situation, there probably also isn't enough free memory for the
308 : * workload that the advice is attempting to manipulate to run
309 : * successfully.
310 : */
311 2 : filebuf = palloc_extended(statbuf.st_size + 1, MCXT_ALLOC_HUGE);
312 2 : nread = fread(filebuf, 1, statbuf.st_size, file);
313 2 : if (ferror(file))
314 0 : ereport(ERROR,
315 : (errcode_for_file_access(),
316 : errmsg("could not read file \"%s\": %m", PGSA_DUMP_FILE)));
317 2 : FreeFile(file);
318 2 : filebuf[nread] = '\0';
319 :
320 : /* Initial memory allocations. */
321 2 : saved_stashes = pgsa_saved_stash_table_create(tmpcxt, 64, NULL);
322 2 : entries = palloc(max_entries * sizeof(pgsa_saved_entry));
323 :
324 : /*
325 : * For memory and CPU efficiency, we parse the file in place. The end of
326 : * each line gets replaced with a NUL byte, and then the end of each field
327 : * within a line gets the same treatment. The advice string is unescaped
328 : * in place, and stash names and query IDs can't contain any special
329 : * characters. All of the resulting pointers point right back into the
330 : * buffer; we only need additional memory to grow the 'entries' array and
331 : * the 'saved_stashes' hash table.
332 : */
333 13 : for (p = filebuf, lineno = 1; *p != '\0'; lineno++)
334 : {
335 11 : char *cursor = p;
336 : char *eol;
337 : char *line_type;
338 :
339 : /* Find end of line and NUL-terminate. */
340 11 : eol = strchr(p, '\n');
341 11 : if (eol != NULL)
342 : {
343 11 : *eol = '\0';
344 11 : p = eol + 1;
345 11 : if (eol > cursor && eol[-1] == '\r')
346 0 : eol[-1] = '\0';
347 : }
348 : else
349 0 : p += strlen(p);
350 :
351 : /* Skip empty lines. */
352 11 : if (*cursor == '\0')
353 0 : continue;
354 :
355 : /* First field is the type of line, either "stash" or "entry". */
356 11 : line_type = pgsa_next_tsv_field(&cursor);
357 11 : if (strcmp(line_type, "stash") == 0)
358 : {
359 : char *name;
360 : bool found;
361 :
362 : /* Second field should be the stash name. */
363 5 : name = pgsa_next_tsv_field(&cursor);
364 5 : if (name == NULL || *name == '\0')
365 0 : ereport(ERROR,
366 : (errcode(ERRCODE_DATA_CORRUPTED),
367 : errmsg("syntax error in file \"%s\" line %u: expected stash name",
368 : PGSA_DUMP_FILE, lineno)));
369 :
370 : /* No further fields are expected. */
371 5 : if (*cursor != '\0')
372 0 : ereport(ERROR,
373 : (errcode(ERRCODE_DATA_CORRUPTED),
374 : errmsg("syntax error in file \"%s\" line %u: expected end of line",
375 : PGSA_DUMP_FILE, lineno)));
376 :
377 : /* Duplicate check. */
378 5 : (void) pgsa_saved_stash_table_insert(saved_stashes, name, &found);
379 5 : if (found)
380 0 : ereport(ERROR,
381 : (errcode(ERRCODE_DATA_CORRUPTED),
382 : errmsg("syntax error in file \"%s\" line %u: duplicate stash name \"%s\"",
383 : PGSA_DUMP_FILE, lineno, name)));
384 5 : num_stashes++;
385 : }
386 6 : else if (strcmp(line_type, "entry") == 0)
387 : {
388 : char *stash_name;
389 : char *queryid_str;
390 : char *advice_str;
391 : char *endptr;
392 : int64 queryId;
393 :
394 : /* Second field should be the stash name. */
395 6 : stash_name = pgsa_next_tsv_field(&cursor);
396 6 : if (stash_name == NULL)
397 0 : ereport(ERROR,
398 : (errcode(ERRCODE_DATA_CORRUPTED),
399 : errmsg("syntax error in file \"%s\" line %u: expected stash name",
400 : PGSA_DUMP_FILE, lineno)));
401 :
402 : /* Third field should be the query ID. */
403 6 : queryid_str = pgsa_next_tsv_field(&cursor);
404 6 : if (queryid_str == NULL)
405 0 : ereport(ERROR,
406 : (errcode(ERRCODE_DATA_CORRUPTED),
407 : errmsg("syntax error in file \"%s\" line %u: expected query ID",
408 : PGSA_DUMP_FILE, lineno)));
409 :
410 : /* Fourth field should be the advice string. */
411 6 : advice_str = pgsa_next_tsv_field(&cursor);
412 6 : if (advice_str == NULL)
413 0 : ereport(ERROR,
414 : (errcode(ERRCODE_DATA_CORRUPTED),
415 : errmsg("syntax error in file \"%s\" line %u: expected advice string",
416 : PGSA_DUMP_FILE, lineno)));
417 :
418 : /* No further fields are expected. */
419 6 : if (*cursor != '\0')
420 0 : ereport(ERROR,
421 : (errcode(ERRCODE_DATA_CORRUPTED),
422 : errmsg("syntax error in file \"%s\" line %u: expected end of line",
423 : PGSA_DUMP_FILE, lineno)));
424 :
425 : /* Make sure the stash is one we've actually seen. */
426 6 : if (pgsa_saved_stash_table_lookup(saved_stashes,
427 : stash_name) == NULL)
428 0 : ereport(ERROR,
429 : (errcode(ERRCODE_DATA_CORRUPTED),
430 : errmsg("syntax error in file \"%s\" line %u: unknown stash \"%s\"",
431 : PGSA_DUMP_FILE, lineno, stash_name)));
432 :
433 : /* Parse the query ID. */
434 6 : errno = 0;
435 6 : queryId = strtoll(queryid_str, &endptr, 10);
436 6 : if (*endptr != '\0' || errno != 0 || queryid_str == endptr ||
437 : queryId == 0)
438 0 : ereport(ERROR,
439 : (errcode(ERRCODE_DATA_CORRUPTED),
440 : errmsg("syntax error in file \"%s\" line %u: invalid query ID \"%s\"",
441 : PGSA_DUMP_FILE, lineno, queryid_str)));
442 :
443 : /* Unescape the advice string. */
444 6 : pgsa_unescape_tsv_field(advice_str, PGSA_DUMP_FILE, lineno);
445 :
446 : /* Append to the entry array. */
447 6 : if (num_entries >= max_entries)
448 : {
449 0 : max_entries *= 2;
450 0 : entries = repalloc(entries,
451 : max_entries * sizeof(pgsa_saved_entry));
452 : }
453 6 : entries[num_entries].stash_name = stash_name;
454 6 : entries[num_entries].queryId = queryId;
455 6 : entries[num_entries].advice_string = advice_str;
456 6 : num_entries++;
457 : }
458 : else
459 : {
460 0 : ereport(ERROR,
461 : (errcode(ERRCODE_DATA_CORRUPTED),
462 : errmsg("syntax error in file \"%s\" line %u: unrecognized line type",
463 : PGSA_DUMP_FILE, lineno)));
464 : }
465 : }
466 :
467 : /*
468 : * Parsing succeeded. Apply everything to shared memory.
469 : *
470 : * At this point, we know that the file we just read is fully valid, but
471 : * it's still possible for this to fail if, for example, DSA memory cannot
472 : * be allocated. If that happens, the worker will die, the postmaster will
473 : * eventually restart it, and we'll try again after clearing any data that
474 : * we did manage to put into shared memory. (Note that we call
475 : * pgsa_reset_all_stashes() at the top of this function.)
476 : */
477 2 : pgsa_restore_stashes(saved_stashes);
478 2 : pgsa_restore_entries(entries, num_entries);
479 :
480 : /* Hooray, it worked! Notify the user. */
481 2 : ereport(LOG,
482 : (errmsg("loaded %d advice stashes and %d entries from \"%s\"",
483 : num_stashes, num_entries, PGSA_DUMP_FILE)));
484 :
485 : /* Clean up. */
486 2 : MemoryContextSwitchTo(oldcxt);
487 2 : MemoryContextDelete(tmpcxt);
488 : }
489 :
490 : /*
491 : * Write all advice stash data to disk.
492 : *
493 : * The file format is a simple TSV with a line-type prefix:
494 : * stash\tstash_name
495 : * entry\tstash_name\tquery_id\tadvice_string
496 : */
497 : static void
498 4 : pgsa_write_to_disk(void)
499 : {
500 4 : pgsa_writer_context wctx = {0};
501 : MemoryContext tmpcxt;
502 : MemoryContext oldcxt;
503 :
504 : Assert(pgsa_entry_dshash != NULL);
505 :
506 : /* Use a temporary context so all allocations are freed at the end. */
507 4 : tmpcxt = AllocSetContextCreate(CurrentMemoryContext,
508 : "pg_stash_advice dump",
509 : ALLOCSET_DEFAULT_SIZES);
510 4 : oldcxt = MemoryContextSwitchTo(tmpcxt);
511 :
512 : /* Set up the writer context. */
513 4 : snprintf(wctx.pathname, MAXPGPATH, "%s.tmp", PGSA_DUMP_FILE);
514 4 : wctx.file = AllocateFile(wctx.pathname, "w");
515 4 : if (!wctx.file)
516 0 : ereport(ERROR,
517 : (errcode_for_file_access(),
518 : errmsg("could not open file \"%s\": %m", wctx.pathname)));
519 4 : wctx.nhash = pgsa_stash_name_table_create(tmpcxt, 64, NULL);
520 4 : initStringInfo(&wctx.buf);
521 :
522 : /* Write stash lines, then entry lines. */
523 4 : pgsa_write_stashes(&wctx);
524 4 : pgsa_write_entries(&wctx);
525 :
526 : /*
527 : * If nothing was written, remove both the temp file and any existing dump
528 : * file rather than installing a zero-length file.
529 : */
530 4 : if (wctx.nhash->members == 0)
531 : {
532 2 : ereport(DEBUG1,
533 : errmsg("there are no advice stashes to save"));
534 2 : FreeFile(wctx.file);
535 2 : unlink(wctx.pathname);
536 2 : if (unlink(PGSA_DUMP_FILE) == 0)
537 1 : ereport(DEBUG1,
538 : errmsg("removed \"%s\"", PGSA_DUMP_FILE));
539 : }
540 : else
541 : {
542 2 : if (FreeFile(wctx.file) != 0)
543 : {
544 0 : int save_errno = errno;
545 :
546 0 : unlink(wctx.pathname);
547 0 : errno = save_errno;
548 0 : ereport(ERROR,
549 : (errcode_for_file_access(),
550 : errmsg("could not close file \"%s\": %m",
551 : wctx.pathname)));
552 : }
553 2 : (void) durable_rename(wctx.pathname, PGSA_DUMP_FILE, ERROR);
554 :
555 2 : ereport(LOG,
556 : errmsg("saved %d advice stashes and %d entries to \"%s\"",
557 : (int) wctx.nhash->members, wctx.entries_written,
558 : PGSA_DUMP_FILE));
559 : }
560 :
561 4 : MemoryContextSwitchTo(oldcxt);
562 4 : MemoryContextDelete(tmpcxt);
563 4 : }
564 :
565 : /*
566 : * Append the TSV-escaped form of str to buf.
567 : *
568 : * Backslash, tab, newline, and carriage return are escaped with backslash
569 : * sequences. All other characters are passed through unchanged.
570 : */
571 : static void
572 6 : pgsa_append_tsv_escaped_string(StringInfo buf, const char *str)
573 : {
574 100 : for (const char *p = str; *p != '\0'; p++)
575 : {
576 94 : switch (*p)
577 : {
578 2 : case '\\':
579 2 : appendStringInfoString(buf, "\\\\");
580 2 : break;
581 2 : case '\t':
582 2 : appendStringInfoString(buf, "\\t");
583 2 : break;
584 2 : case '\n':
585 2 : appendStringInfoString(buf, "\\n");
586 2 : break;
587 0 : case '\r':
588 0 : appendStringInfoString(buf, "\\r");
589 0 : break;
590 88 : default:
591 88 : appendStringInfoChar(buf, *p);
592 88 : break;
593 : }
594 : }
595 6 : }
596 :
597 : /*
598 : * Extract the next tab-delimited field from *cursor.
599 : *
600 : * The tab delimiter is replaced with '\0' and *cursor is advanced past it.
601 : * If *cursor already points to '\0' (no more fields), returns NULL.
602 : */
603 : static char *
604 34 : pgsa_next_tsv_field(char **cursor)
605 : {
606 34 : char *start = *cursor;
607 34 : char *p = start;
608 :
609 34 : if (*p == '\0')
610 0 : return NULL;
611 :
612 290 : while (*p != '\0' && *p != '\t')
613 256 : p++;
614 :
615 34 : if (*p == '\t')
616 23 : *p++ = '\0';
617 :
618 34 : *cursor = p;
619 34 : return start;
620 : }
621 :
622 : /*
623 : * Insert entries into shared memory from the parsed entry array.
624 : */
625 : static void
626 2 : pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries)
627 : {
628 2 : LWLockAcquire(&pgsa_state->lock, LW_SHARED);
629 8 : for (int i = 0; i < num_entries; i++)
630 : {
631 6 : ereport(DEBUG2,
632 : errmsg("restoring advice stash entry for \"%s\", query ID %" PRId64,
633 : entries[i].stash_name, entries[i].queryId));
634 6 : pgsa_set_advice_string(entries[i].stash_name,
635 6 : entries[i].queryId,
636 6 : entries[i].advice_string);
637 : }
638 2 : LWLockRelease(&pgsa_state->lock);
639 2 : }
640 :
641 : /*
642 : * Create stashes in shared memory from the parsed stash hash table.
643 : */
644 : static void
645 2 : pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes)
646 : {
647 : pgsa_saved_stash_table_iterator iter;
648 : pgsa_saved_stash *s;
649 :
650 2 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
651 2 : pgsa_saved_stash_table_start_iterate(saved_stashes, &iter);
652 7 : while ((s = pgsa_saved_stash_table_iterate(saved_stashes,
653 7 : &iter)) != NULL)
654 : {
655 5 : ereport(DEBUG2,
656 : errmsg("restoring advice stash \"%s\"", s->name));
657 5 : pgsa_create_stash(s->name);
658 : }
659 2 : LWLockRelease(&pgsa_state->lock);
660 2 : }
661 :
662 : /*
663 : * Unescape a TSV field in place.
664 : *
665 : * Recognized escape sequences are \\, \t, \n, and \r. A trailing backslash
666 : * or an unrecognized escape sequence is a syntax error.
667 : */
668 : static void
669 6 : pgsa_unescape_tsv_field(char *str, const char *filename, unsigned lineno)
670 : {
671 6 : char *src = str;
672 6 : char *dst = str;
673 :
674 100 : while (*src != '\0')
675 : {
676 : /* Just pass through anything that's not a backslash-escape. */
677 94 : if (likely(*src != '\\'))
678 : {
679 88 : *dst++ = *src++;
680 88 : continue;
681 : }
682 :
683 : /* Check what sort of escape we've got. */
684 6 : switch (src[1])
685 : {
686 2 : case '\\':
687 2 : *dst++ = '\\';
688 2 : break;
689 2 : case 't':
690 2 : *dst++ = '\t';
691 2 : break;
692 2 : case 'n':
693 2 : *dst++ = '\n';
694 2 : break;
695 0 : case 'r':
696 0 : *dst++ = '\r';
697 0 : break;
698 0 : case '\0':
699 0 : ereport(ERROR,
700 : (errcode(ERRCODE_DATA_CORRUPTED),
701 : errmsg("syntax error in file \"%s\" line %u: trailing backslash",
702 : filename, lineno)));
703 : break;
704 0 : default:
705 0 : ereport(ERROR,
706 : (errcode(ERRCODE_DATA_CORRUPTED),
707 : errmsg("syntax error in file \"%s\" line %u: unrecognized escape \"\\%c\"",
708 : filename, lineno, src[1])));
709 : break;
710 : }
711 :
712 : /* We consumed the backslash and the following character. */
713 6 : src += 2;
714 : }
715 6 : *dst = '\0';
716 6 : }
717 :
718 : /*
719 : * Write an entry line for each advice entry.
720 : */
721 : static void
722 4 : pgsa_write_entries(pgsa_writer_context *wctx)
723 : {
724 : dshash_seq_status iter;
725 : pgsa_entry *entry;
726 :
727 4 : dshash_seq_init(&iter, pgsa_entry_dshash, false);
728 10 : while ((entry = dshash_seq_next(&iter)) != NULL)
729 : {
730 : pgsa_stash_name *n;
731 : char *advice_string;
732 :
733 6 : if (entry->advice_string == InvalidDsaPointer)
734 0 : continue;
735 :
736 6 : n = pgsa_stash_name_table_lookup(wctx->nhash,
737 : entry->key.pgsa_stash_id);
738 6 : if (n == NULL)
739 0 : continue; /* orphan entry, skip */
740 :
741 6 : advice_string = dsa_get_address(pgsa_dsa_area, entry->advice_string);
742 :
743 6 : resetStringInfo(&wctx->buf);
744 6 : appendStringInfo(&wctx->buf, "entry\t%s\t%" PRId64 "\t",
745 : n->name, entry->key.queryId);
746 6 : pgsa_append_tsv_escaped_string(&wctx->buf, advice_string);
747 6 : appendStringInfoChar(&wctx->buf, '\n');
748 6 : fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file);
749 6 : if (ferror(wctx->file))
750 0 : pgsa_write_error(wctx);
751 6 : wctx->entries_written++;
752 : }
753 4 : dshash_seq_term(&iter);
754 4 : }
755 :
756 : /*
757 : * Clean up and report a write error. Does not return.
758 : */
759 : static void
760 0 : pgsa_write_error(pgsa_writer_context *wctx)
761 : {
762 0 : int save_errno = errno;
763 :
764 0 : FreeFile(wctx->file);
765 0 : unlink(wctx->pathname);
766 0 : errno = save_errno;
767 0 : ereport(ERROR,
768 : (errcode_for_file_access(),
769 : errmsg("could not write to file \"%s\": %m", wctx->pathname)));
770 : }
771 :
772 : /*
773 : * Write a stash line for each advice stash, and populate the ID-to-name
774 : * hash table for use by pgsa_write_entries.
775 : */
776 : static void
777 4 : pgsa_write_stashes(pgsa_writer_context *wctx)
778 : {
779 : dshash_seq_status iter;
780 : pgsa_stash *stash;
781 :
782 4 : dshash_seq_init(&iter, pgsa_stash_dshash, false);
783 9 : while ((stash = dshash_seq_next(&iter)) != NULL)
784 : {
785 : pgsa_stash_name *n;
786 : bool found;
787 :
788 5 : n = pgsa_stash_name_table_insert(wctx->nhash, stash->pgsa_stash_id,
789 : &found);
790 : Assert(!found);
791 5 : n->name = pstrdup(stash->name);
792 :
793 5 : resetStringInfo(&wctx->buf);
794 5 : appendStringInfo(&wctx->buf, "stash\t%s\n", n->name);
795 5 : fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file);
796 5 : if (ferror(wctx->file))
797 0 : pgsa_write_error(wctx);
798 : }
799 4 : dshash_seq_term(&iter);
800 4 : }
|