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