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
53 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 : : {
53 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 : : }
53 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 : : {
53 rhaas@postgresql.org 159 :UNC 0 : ConfigReloadPending = false;
160 : 0 : ProcessConfigFile(PGC_SIGHUP);
161 : : }
162 : :
53 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. */
53 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 : :
53 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;
53 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. */
53 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)
53 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 : : */
53 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))
53 rhaas@postgresql.org 314 [ # # ]:UNC 0 : ereport(ERROR,
315 : : (errcode_for_file_access(),
316 : : errmsg("could not read file \"%s\": %m", PGSA_DUMP_FILE)));
53 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')
53 rhaas@postgresql.org 346 :UNC 0 : eol[-1] = '\0';
347 : : }
348 : : else
349 : 0 : p += strlen(p);
350 : :
351 : : /* Skip empty lines. */
53 rhaas@postgresql.org 352 [ - + ]:GNC 11 : if (*cursor == '\0')
53 rhaas@postgresql.org 353 :UNC 0 : continue;
354 : :
355 : : /* First field is the type of line, either "stash" or "entry". */
53 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')
53 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. */
53 rhaas@postgresql.org 371 [ - + ]:GNC 5 : if (*cursor != '\0')
53 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 : : /* Reject overlong stash names. */
1 rhaas@postgresql.org 378 [ - + ]:GNC 5 : if (strlen(name) >= NAMEDATALEN)
1 rhaas@postgresql.org 379 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 385 :GNC 5 : (void) pgsa_saved_stash_table_insert(saved_stashes, name, &found);
386 [ - + ]: 5 : if (found)
53 rhaas@postgresql.org 387 [ # # ]:UNC 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)));
53 rhaas@postgresql.org 391 :GNC 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)
53 rhaas@postgresql.org 404 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 410 :GNC 6 : queryid_str = pgsa_next_tsv_field(&cursor);
411 [ - + ]: 6 : if (queryid_str == NULL)
53 rhaas@postgresql.org 412 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 418 :GNC 6 : advice_str = pgsa_next_tsv_field(&cursor);
419 [ - + ]: 6 : if (advice_str == NULL)
53 rhaas@postgresql.org 420 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 426 [ - + ]:GNC 6 : if (*cursor != '\0')
53 rhaas@postgresql.org 427 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 433 [ - + ]:GNC 6 : if (pgsa_saved_stash_table_lookup(saved_stashes,
434 : : stash_name) == NULL)
53 rhaas@postgresql.org 435 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 441 :GNC 6 : errno = 0;
3 peter@eisentraut.org 442 : 6 : queryId = strtoi64(queryid_str, &endptr, 10);
53 rhaas@postgresql.org 443 [ + - + - : 6 : if (*endptr != '\0' || errno != 0 || queryid_str == endptr ||
+ - - + ]
444 : : queryId == 0)
53 rhaas@postgresql.org 445 [ # # ]:UNC 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. */
53 rhaas@postgresql.org 451 :GNC 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 : : {
53 rhaas@postgresql.org 456 :UNC 0 : max_entries *= 2;
457 : 0 : entries = repalloc(entries,
458 : : max_entries * sizeof(pgsa_saved_entry));
459 : : }
53 rhaas@postgresql.org 460 :GNC 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 : : {
53 rhaas@postgresql.org 467 [ # # ]:UNC 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 : : */
53 rhaas@postgresql.org 484 :GNC 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 [ - + ]: 4 : 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)
53 rhaas@postgresql.org 523 [ # # ]:UNC 0 : ereport(ERROR,
524 : : (errcode_for_file_access(),
525 : : errmsg("could not open file \"%s\": %m", wctx.pathname)));
53 rhaas@postgresql.org 526 :GNC 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 : : {
53 rhaas@postgresql.org 551 :UNC 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 : : }
53 rhaas@postgresql.org 560 :GNC 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;
53 rhaas@postgresql.org 594 :UNC 0 : case '\r':
595 : 0 : appendStringInfoString(buf, "\\r");
596 : 0 : break;
53 rhaas@postgresql.org 597 :GNC 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')
53 rhaas@postgresql.org 617 :UNC 0 : return NULL;
618 : :
53 rhaas@postgresql.org 619 [ + + + + ]:GNC 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;
53 rhaas@postgresql.org 702 :UNC 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. */
53 rhaas@postgresql.org 720 :GNC 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)
53 rhaas@postgresql.org 741 :UNC 0 : continue;
742 : :
53 rhaas@postgresql.org 743 :GNC 6 : n = pgsa_stash_name_table_lookup(wctx->nhash,
744 : : entry->key.pgsa_stash_id);
745 [ - + ]: 6 : if (n == NULL)
53 rhaas@postgresql.org 746 :UNC 0 : continue; /* orphan entry, skip */
747 : :
53 rhaas@postgresql.org 748 :GNC 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))
53 rhaas@postgresql.org 757 :UNC 0 : pgsa_write_error(wctx);
53 rhaas@postgresql.org 758 :GNC 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
53 rhaas@postgresql.org 767 :UNC 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
53 rhaas@postgresql.org 784 :GNC 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 [ - + ]: 5 : 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))
53 rhaas@postgresql.org 804 :UNC 0 : pgsa_write_error(wctx);
805 : : }
53 rhaas@postgresql.org 806 :GNC 4 : dshash_seq_term(&iter);
807 : 4 : }
|