Age Owner Branch data TLA 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
28 rhaas@postgresql.org 94 :GNC 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 : : {
28 rhaas@postgresql.org 130 :UNC 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 : : }
28 rhaas@postgresql.org 136 :GNC 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 : : {
28 rhaas@postgresql.org 159 :UNC 0 : ConfigReloadPending = false;
160 : 0 : ProcessConfigFile(PGC_SIGHUP);
161 : : }
162 : :
28 rhaas@postgresql.org 163 [ + - ]:GNC 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. */
28 rhaas@postgresql.org 178 :UNC 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 : :
28 rhaas@postgresql.org 210 :GNC 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 [ - + ]: 4 : 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;
28 rhaas@postgresql.org 269 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 275 :GNC 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)
28 rhaas@postgresql.org 282 [ # # ]:UNC 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 : : */
28 rhaas@postgresql.org 311 :GNC 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))
28 rhaas@postgresql.org 314 [ # # ]:UNC 0 : ereport(ERROR,
315 : : (errcode_for_file_access(),
316 : : errmsg("could not read file \"%s\": %m", PGSA_DUMP_FILE)));
28 rhaas@postgresql.org 317 :GNC 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')
28 rhaas@postgresql.org 346 :UNC 0 : eol[-1] = '\0';
347 : : }
348 : : else
349 : 0 : p += strlen(p);
350 : :
351 : : /* Skip empty lines. */
28 rhaas@postgresql.org 352 [ - + ]:GNC 11 : if (*cursor == '\0')
28 rhaas@postgresql.org 353 :UNC 0 : continue;
354 : :
355 : : /* First field is the type of line, either "stash" or "entry". */
28 rhaas@postgresql.org 356 :GNC 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')
28 rhaas@postgresql.org 365 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 371 [ - + ]:GNC 5 : if (*cursor != '\0')
28 rhaas@postgresql.org 372 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 378 :GNC 5 : (void) pgsa_saved_stash_table_insert(saved_stashes, name, &found);
379 [ - + ]: 5 : if (found)
28 rhaas@postgresql.org 380 [ # # ]:UNC 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)));
28 rhaas@postgresql.org 384 :GNC 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)
28 rhaas@postgresql.org 397 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 403 :GNC 6 : queryid_str = pgsa_next_tsv_field(&cursor);
404 [ - + ]: 6 : if (queryid_str == NULL)
28 rhaas@postgresql.org 405 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 411 :GNC 6 : advice_str = pgsa_next_tsv_field(&cursor);
412 [ - + ]: 6 : if (advice_str == NULL)
28 rhaas@postgresql.org 413 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 419 [ - + ]:GNC 6 : if (*cursor != '\0')
28 rhaas@postgresql.org 420 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 426 [ - + ]:GNC 6 : if (pgsa_saved_stash_table_lookup(saved_stashes,
427 : : stash_name) == NULL)
28 rhaas@postgresql.org 428 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 434 :GNC 6 : errno = 0;
435 : 6 : queryId = strtoll(queryid_str, &endptr, 10);
436 [ + - + - : 6 : if (*endptr != '\0' || errno != 0 || queryid_str == endptr ||
+ - - + ]
437 : : queryId == 0)
28 rhaas@postgresql.org 438 [ # # ]:UNC 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. */
28 rhaas@postgresql.org 444 :GNC 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 : : {
28 rhaas@postgresql.org 449 :UNC 0 : max_entries *= 2;
450 : 0 : entries = repalloc(entries,
451 : : max_entries * sizeof(pgsa_saved_entry));
452 : : }
28 rhaas@postgresql.org 453 :GNC 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 : : {
28 rhaas@postgresql.org 460 [ # # ]:UNC 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 : : */
28 rhaas@postgresql.org 477 :GNC 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 [ - + ]: 4 : 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)
28 rhaas@postgresql.org 516 [ # # ]:UNC 0 : ereport(ERROR,
517 : : (errcode_for_file_access(),
518 : : errmsg("could not open file \"%s\": %m", wctx.pathname)));
28 rhaas@postgresql.org 519 :GNC 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 : : {
28 rhaas@postgresql.org 544 :UNC 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 : : }
28 rhaas@postgresql.org 553 :GNC 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;
28 rhaas@postgresql.org 587 :UNC 0 : case '\r':
588 : 0 : appendStringInfoString(buf, "\\r");
589 : 0 : break;
28 rhaas@postgresql.org 590 :GNC 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')
28 rhaas@postgresql.org 610 :UNC 0 : return NULL;
611 : :
28 rhaas@postgresql.org 612 [ + + + + ]:GNC 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;
28 rhaas@postgresql.org 695 :UNC 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. */
28 rhaas@postgresql.org 713 :GNC 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)
28 rhaas@postgresql.org 734 :UNC 0 : continue;
735 : :
28 rhaas@postgresql.org 736 :GNC 6 : n = pgsa_stash_name_table_lookup(wctx->nhash,
737 : : entry->key.pgsa_stash_id);
738 [ - + ]: 6 : if (n == NULL)
28 rhaas@postgresql.org 739 :UNC 0 : continue; /* orphan entry, skip */
740 : :
28 rhaas@postgresql.org 741 :GNC 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))
28 rhaas@postgresql.org 750 :UNC 0 : pgsa_write_error(wctx);
28 rhaas@postgresql.org 751 :GNC 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
28 rhaas@postgresql.org 760 :UNC 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
28 rhaas@postgresql.org 777 :GNC 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 [ - + ]: 5 : 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))
28 rhaas@postgresql.org 797 :UNC 0 : pgsa_write_error(wctx);
798 : : }
28 rhaas@postgresql.org 799 :GNC 4 : dshash_seq_term(&iter);
800 : 4 : }
|