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 : /* Reject overlong stash names. */
378 5 : if (strlen(name) >= NAMEDATALEN)
379 0 : ereport(ERROR,
380 : (errcode(ERRCODE_DATA_CORRUPTED),
381 : errmsg("syntax error in file \"%s\" line %u: stash name too long",
382 : PGSA_DUMP_FILE, lineno)));
383 :
384 : /* Duplicate check. */
385 5 : (void) pgsa_saved_stash_table_insert(saved_stashes, name, &found);
386 5 : if (found)
387 0 : ereport(ERROR,
388 : (errcode(ERRCODE_DATA_CORRUPTED),
389 : errmsg("syntax error in file \"%s\" line %u: duplicate stash name \"%s\"",
390 : PGSA_DUMP_FILE, lineno, name)));
391 5 : num_stashes++;
392 : }
393 6 : else if (strcmp(line_type, "entry") == 0)
394 : {
395 : char *stash_name;
396 : char *queryid_str;
397 : char *advice_str;
398 : char *endptr;
399 : int64 queryId;
400 :
401 : /* Second field should be the stash name. */
402 6 : stash_name = pgsa_next_tsv_field(&cursor);
403 6 : if (stash_name == NULL)
404 0 : ereport(ERROR,
405 : (errcode(ERRCODE_DATA_CORRUPTED),
406 : errmsg("syntax error in file \"%s\" line %u: expected stash name",
407 : PGSA_DUMP_FILE, lineno)));
408 :
409 : /* Third field should be the query ID. */
410 6 : queryid_str = pgsa_next_tsv_field(&cursor);
411 6 : if (queryid_str == NULL)
412 0 : ereport(ERROR,
413 : (errcode(ERRCODE_DATA_CORRUPTED),
414 : errmsg("syntax error in file \"%s\" line %u: expected query ID",
415 : PGSA_DUMP_FILE, lineno)));
416 :
417 : /* Fourth field should be the advice string. */
418 6 : advice_str = pgsa_next_tsv_field(&cursor);
419 6 : if (advice_str == NULL)
420 0 : ereport(ERROR,
421 : (errcode(ERRCODE_DATA_CORRUPTED),
422 : errmsg("syntax error in file \"%s\" line %u: expected advice string",
423 : PGSA_DUMP_FILE, lineno)));
424 :
425 : /* No further fields are expected. */
426 6 : if (*cursor != '\0')
427 0 : ereport(ERROR,
428 : (errcode(ERRCODE_DATA_CORRUPTED),
429 : errmsg("syntax error in file \"%s\" line %u: expected end of line",
430 : PGSA_DUMP_FILE, lineno)));
431 :
432 : /* Make sure the stash is one we've actually seen. */
433 6 : if (pgsa_saved_stash_table_lookup(saved_stashes,
434 : stash_name) == NULL)
435 0 : ereport(ERROR,
436 : (errcode(ERRCODE_DATA_CORRUPTED),
437 : errmsg("syntax error in file \"%s\" line %u: unknown stash \"%s\"",
438 : PGSA_DUMP_FILE, lineno, stash_name)));
439 :
440 : /* Parse the query ID. */
441 6 : errno = 0;
442 6 : queryId = strtoi64(queryid_str, &endptr, 10);
443 6 : if (*endptr != '\0' || errno != 0 || queryid_str == endptr ||
444 : queryId == 0)
445 0 : ereport(ERROR,
446 : (errcode(ERRCODE_DATA_CORRUPTED),
447 : errmsg("syntax error in file \"%s\" line %u: invalid query ID \"%s\"",
448 : PGSA_DUMP_FILE, lineno, queryid_str)));
449 :
450 : /* Unescape the advice string. */
451 6 : pgsa_unescape_tsv_field(advice_str, PGSA_DUMP_FILE, lineno);
452 :
453 : /* Append to the entry array. */
454 6 : if (num_entries >= max_entries)
455 : {
456 0 : max_entries *= 2;
457 0 : entries = repalloc(entries,
458 : max_entries * sizeof(pgsa_saved_entry));
459 : }
460 6 : entries[num_entries].stash_name = stash_name;
461 6 : entries[num_entries].queryId = queryId;
462 6 : entries[num_entries].advice_string = advice_str;
463 6 : num_entries++;
464 : }
465 : else
466 : {
467 0 : ereport(ERROR,
468 : (errcode(ERRCODE_DATA_CORRUPTED),
469 : errmsg("syntax error in file \"%s\" line %u: unrecognized line type",
470 : PGSA_DUMP_FILE, lineno)));
471 : }
472 : }
473 :
474 : /*
475 : * Parsing succeeded. Apply everything to shared memory.
476 : *
477 : * At this point, we know that the file we just read is fully valid, but
478 : * it's still possible for this to fail if, for example, DSA memory cannot
479 : * be allocated. If that happens, the worker will die, the postmaster will
480 : * eventually restart it, and we'll try again after clearing any data that
481 : * we did manage to put into shared memory. (Note that we call
482 : * pgsa_reset_all_stashes() at the top of this function.)
483 : */
484 2 : pgsa_restore_stashes(saved_stashes);
485 2 : pgsa_restore_entries(entries, num_entries);
486 :
487 : /* Hooray, it worked! Notify the user. */
488 2 : ereport(LOG,
489 : (errmsg("loaded %d advice stashes and %d entries from \"%s\"",
490 : num_stashes, num_entries, PGSA_DUMP_FILE)));
491 :
492 : /* Clean up. */
493 2 : MemoryContextSwitchTo(oldcxt);
494 2 : MemoryContextDelete(tmpcxt);
495 : }
496 :
497 : /*
498 : * Write all advice stash data to disk.
499 : *
500 : * The file format is a simple TSV with a line-type prefix:
501 : * stash\tstash_name
502 : * entry\tstash_name\tquery_id\tadvice_string
503 : */
504 : static void
505 4 : pgsa_write_to_disk(void)
506 : {
507 4 : pgsa_writer_context wctx = {0};
508 : MemoryContext tmpcxt;
509 : MemoryContext oldcxt;
510 :
511 : Assert(pgsa_entry_dshash != NULL);
512 :
513 : /* Use a temporary context so all allocations are freed at the end. */
514 4 : tmpcxt = AllocSetContextCreate(CurrentMemoryContext,
515 : "pg_stash_advice dump",
516 : ALLOCSET_DEFAULT_SIZES);
517 4 : oldcxt = MemoryContextSwitchTo(tmpcxt);
518 :
519 : /* Set up the writer context. */
520 4 : snprintf(wctx.pathname, MAXPGPATH, "%s.tmp", PGSA_DUMP_FILE);
521 4 : wctx.file = AllocateFile(wctx.pathname, "w");
522 4 : if (!wctx.file)
523 0 : ereport(ERROR,
524 : (errcode_for_file_access(),
525 : errmsg("could not open file \"%s\": %m", wctx.pathname)));
526 4 : wctx.nhash = pgsa_stash_name_table_create(tmpcxt, 64, NULL);
527 4 : initStringInfo(&wctx.buf);
528 :
529 : /* Write stash lines, then entry lines. */
530 4 : pgsa_write_stashes(&wctx);
531 4 : pgsa_write_entries(&wctx);
532 :
533 : /*
534 : * If nothing was written, remove both the temp file and any existing dump
535 : * file rather than installing a zero-length file.
536 : */
537 4 : if (wctx.nhash->members == 0)
538 : {
539 2 : ereport(DEBUG1,
540 : errmsg("there are no advice stashes to save"));
541 2 : FreeFile(wctx.file);
542 2 : unlink(wctx.pathname);
543 2 : if (unlink(PGSA_DUMP_FILE) == 0)
544 1 : ereport(DEBUG1,
545 : errmsg("removed \"%s\"", PGSA_DUMP_FILE));
546 : }
547 : else
548 : {
549 2 : if (FreeFile(wctx.file) != 0)
550 : {
551 0 : int save_errno = errno;
552 :
553 0 : unlink(wctx.pathname);
554 0 : errno = save_errno;
555 0 : ereport(ERROR,
556 : (errcode_for_file_access(),
557 : errmsg("could not close file \"%s\": %m",
558 : wctx.pathname)));
559 : }
560 2 : (void) durable_rename(wctx.pathname, PGSA_DUMP_FILE, ERROR);
561 :
562 2 : ereport(LOG,
563 : errmsg("saved %d advice stashes and %d entries to \"%s\"",
564 : (int) wctx.nhash->members, wctx.entries_written,
565 : PGSA_DUMP_FILE));
566 : }
567 :
568 4 : MemoryContextSwitchTo(oldcxt);
569 4 : MemoryContextDelete(tmpcxt);
570 4 : }
571 :
572 : /*
573 : * Append the TSV-escaped form of str to buf.
574 : *
575 : * Backslash, tab, newline, and carriage return are escaped with backslash
576 : * sequences. All other characters are passed through unchanged.
577 : */
578 : static void
579 6 : pgsa_append_tsv_escaped_string(StringInfo buf, const char *str)
580 : {
581 100 : for (const char *p = str; *p != '\0'; p++)
582 : {
583 94 : switch (*p)
584 : {
585 2 : case '\\':
586 2 : appendStringInfoString(buf, "\\\\");
587 2 : break;
588 2 : case '\t':
589 2 : appendStringInfoString(buf, "\\t");
590 2 : break;
591 2 : case '\n':
592 2 : appendStringInfoString(buf, "\\n");
593 2 : break;
594 0 : case '\r':
595 0 : appendStringInfoString(buf, "\\r");
596 0 : break;
597 88 : default:
598 88 : appendStringInfoChar(buf, *p);
599 88 : break;
600 : }
601 : }
602 6 : }
603 :
604 : /*
605 : * Extract the next tab-delimited field from *cursor.
606 : *
607 : * The tab delimiter is replaced with '\0' and *cursor is advanced past it.
608 : * If *cursor already points to '\0' (no more fields), returns NULL.
609 : */
610 : static char *
611 34 : pgsa_next_tsv_field(char **cursor)
612 : {
613 34 : char *start = *cursor;
614 34 : char *p = start;
615 :
616 34 : if (*p == '\0')
617 0 : return NULL;
618 :
619 290 : while (*p != '\0' && *p != '\t')
620 256 : p++;
621 :
622 34 : if (*p == '\t')
623 23 : *p++ = '\0';
624 :
625 34 : *cursor = p;
626 34 : return start;
627 : }
628 :
629 : /*
630 : * Insert entries into shared memory from the parsed entry array.
631 : */
632 : static void
633 2 : pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries)
634 : {
635 2 : LWLockAcquire(&pgsa_state->lock, LW_SHARED);
636 8 : for (int i = 0; i < num_entries; i++)
637 : {
638 6 : ereport(DEBUG2,
639 : errmsg("restoring advice stash entry for \"%s\", query ID %" PRId64,
640 : entries[i].stash_name, entries[i].queryId));
641 6 : pgsa_set_advice_string(entries[i].stash_name,
642 6 : entries[i].queryId,
643 6 : entries[i].advice_string);
644 : }
645 2 : LWLockRelease(&pgsa_state->lock);
646 2 : }
647 :
648 : /*
649 : * Create stashes in shared memory from the parsed stash hash table.
650 : */
651 : static void
652 2 : pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes)
653 : {
654 : pgsa_saved_stash_table_iterator iter;
655 : pgsa_saved_stash *s;
656 :
657 2 : LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
658 2 : pgsa_saved_stash_table_start_iterate(saved_stashes, &iter);
659 7 : while ((s = pgsa_saved_stash_table_iterate(saved_stashes,
660 7 : &iter)) != NULL)
661 : {
662 5 : ereport(DEBUG2,
663 : errmsg("restoring advice stash \"%s\"", s->name));
664 5 : pgsa_create_stash(s->name);
665 : }
666 2 : LWLockRelease(&pgsa_state->lock);
667 2 : }
668 :
669 : /*
670 : * Unescape a TSV field in place.
671 : *
672 : * Recognized escape sequences are \\, \t, \n, and \r. A trailing backslash
673 : * or an unrecognized escape sequence is a syntax error.
674 : */
675 : static void
676 6 : pgsa_unescape_tsv_field(char *str, const char *filename, unsigned lineno)
677 : {
678 6 : char *src = str;
679 6 : char *dst = str;
680 :
681 100 : while (*src != '\0')
682 : {
683 : /* Just pass through anything that's not a backslash-escape. */
684 94 : if (likely(*src != '\\'))
685 : {
686 88 : *dst++ = *src++;
687 88 : continue;
688 : }
689 :
690 : /* Check what sort of escape we've got. */
691 6 : switch (src[1])
692 : {
693 2 : case '\\':
694 2 : *dst++ = '\\';
695 2 : break;
696 2 : case 't':
697 2 : *dst++ = '\t';
698 2 : break;
699 2 : case 'n':
700 2 : *dst++ = '\n';
701 2 : break;
702 0 : case 'r':
703 0 : *dst++ = '\r';
704 0 : break;
705 0 : case '\0':
706 0 : ereport(ERROR,
707 : (errcode(ERRCODE_DATA_CORRUPTED),
708 : errmsg("syntax error in file \"%s\" line %u: trailing backslash",
709 : filename, lineno)));
710 : break;
711 0 : default:
712 0 : ereport(ERROR,
713 : (errcode(ERRCODE_DATA_CORRUPTED),
714 : errmsg("syntax error in file \"%s\" line %u: unrecognized escape \"\\%c\"",
715 : filename, lineno, src[1])));
716 : break;
717 : }
718 :
719 : /* We consumed the backslash and the following character. */
720 6 : src += 2;
721 : }
722 6 : *dst = '\0';
723 6 : }
724 :
725 : /*
726 : * Write an entry line for each advice entry.
727 : */
728 : static void
729 4 : pgsa_write_entries(pgsa_writer_context *wctx)
730 : {
731 : dshash_seq_status iter;
732 : pgsa_entry *entry;
733 :
734 4 : dshash_seq_init(&iter, pgsa_entry_dshash, false);
735 10 : while ((entry = dshash_seq_next(&iter)) != NULL)
736 : {
737 : pgsa_stash_name *n;
738 : char *advice_string;
739 :
740 6 : if (entry->advice_string == InvalidDsaPointer)
741 0 : continue;
742 :
743 6 : n = pgsa_stash_name_table_lookup(wctx->nhash,
744 : entry->key.pgsa_stash_id);
745 6 : if (n == NULL)
746 0 : continue; /* orphan entry, skip */
747 :
748 6 : advice_string = dsa_get_address(pgsa_dsa_area, entry->advice_string);
749 :
750 6 : resetStringInfo(&wctx->buf);
751 6 : appendStringInfo(&wctx->buf, "entry\t%s\t%" PRId64 "\t",
752 : n->name, entry->key.queryId);
753 6 : pgsa_append_tsv_escaped_string(&wctx->buf, advice_string);
754 6 : appendStringInfoChar(&wctx->buf, '\n');
755 6 : fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file);
756 6 : if (ferror(wctx->file))
757 0 : pgsa_write_error(wctx);
758 6 : wctx->entries_written++;
759 : }
760 4 : dshash_seq_term(&iter);
761 4 : }
762 :
763 : /*
764 : * Clean up and report a write error. Does not return.
765 : */
766 : static void
767 0 : pgsa_write_error(pgsa_writer_context *wctx)
768 : {
769 0 : int save_errno = errno;
770 :
771 0 : FreeFile(wctx->file);
772 0 : unlink(wctx->pathname);
773 0 : errno = save_errno;
774 0 : ereport(ERROR,
775 : (errcode_for_file_access(),
776 : errmsg("could not write to file \"%s\": %m", wctx->pathname)));
777 : }
778 :
779 : /*
780 : * Write a stash line for each advice stash, and populate the ID-to-name
781 : * hash table for use by pgsa_write_entries.
782 : */
783 : static void
784 4 : pgsa_write_stashes(pgsa_writer_context *wctx)
785 : {
786 : dshash_seq_status iter;
787 : pgsa_stash *stash;
788 :
789 4 : dshash_seq_init(&iter, pgsa_stash_dshash, false);
790 9 : while ((stash = dshash_seq_next(&iter)) != NULL)
791 : {
792 : pgsa_stash_name *n;
793 : bool found;
794 :
795 5 : n = pgsa_stash_name_table_insert(wctx->nhash, stash->pgsa_stash_id,
796 : &found);
797 : Assert(!found);
798 5 : n->name = pstrdup(stash->name);
799 :
800 5 : resetStringInfo(&wctx->buf);
801 5 : appendStringInfo(&wctx->buf, "stash\t%s\n", n->name);
802 5 : fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file);
803 5 : if (ferror(wctx->file))
804 0 : pgsa_write_error(wctx);
805 : }
806 4 : dshash_seq_term(&iter);
807 4 : }
|