1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00
Files
postgres/contrib/pg_stash_advice/stashpersist.c
Robert Haas dc33cca185 pg_stash_advice: Fix failure to reload empty advice string.
There's not really any good reason to store an empty advice string
in an advice stash, but it's entirely possible that some people might
do it by accident. If they do that without this fix, restarting the
system will result in failure while reloading pg_stash_advice.tsv.

The root cause is that I (rhaas) did not think clearly enough when
defining pgsa_next_tsv_field(). After returning each field on the
line, it set *cursor to the start of the next field, and after
returning all of them, it set *cursor to the trailing NUL byte. But
when the last field on the line is empty, this is ambiguous: if we're
pointing at the trailing NUL byte, it could be either because it's the
start of the zero-length field or because we're out of fields. To
remove the ambiguity, this commit decides that *cursor will instead
be set to NULL when there are no more fields.

Reported-by: Noah Misch <noah@leadboat.com>
Discussion: https://postgr.es/m/20260827171830.68.noahmisch@microsoft.com
Backpatch-through: 19
2026-09-08 10:03:52 -04:00

816 lines
23 KiB
C

/*-------------------------------------------------------------------------
*
* stashpersist.c
* Persistence support for pg_stash_advice.
*
* Copyright (c) 2016-2026, PostgreSQL Global Development Group
*
* contrib/pg_stash_advice/stashpersist.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include <sys/stat.h>
#include "common/hashfn.h"
#include "miscadmin.h"
#include "pg_stash_advice.h"
#include "postmaster/bgworker.h"
#include "postmaster/interrupt.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/latch.h"
#include "storage/proc.h"
#include "storage/procsignal.h"
#include "utils/backend_status.h"
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/timestamp.h"
typedef struct pgsa_writer_context
{
char pathname[MAXPGPATH];
FILE *file;
pgsa_stash_name_table_hash *nhash;
StringInfoData buf;
int entries_written;
} pgsa_writer_context;
/*
* A parsed entry line, with pointers into the slurp buffer.
*/
typedef struct pgsa_saved_entry
{
char *stash_name;
int64 queryId;
char *advice_string;
} pgsa_saved_entry;
/*
* simplehash for detecting duplicate stash names during parsing.
* Keyed by stash name (char *), pointing into the slurp buffer.
*/
typedef struct pgsa_saved_stash
{
uint32 status;
char *name;
} pgsa_saved_stash;
#define SH_PREFIX pgsa_saved_stash_table
#define SH_ELEMENT_TYPE pgsa_saved_stash
#define SH_KEY_TYPE char *
#define SH_KEY name
#define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) (key), strlen(key))
#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
#define SH_SCOPE static inline
#define SH_DEFINE
#define SH_DECLARE
#include "lib/simplehash.h"
extern PGDLLEXPORT void pg_stash_advice_worker_main(Datum main_arg);
static void pgsa_append_tsv_escaped_string(StringInfo buf, const char *str);
static void pgsa_detach_shmem(int code, Datum arg);
static char *pgsa_next_tsv_field(char **cursor);
static void pgsa_read_from_disk(void);
static void pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries);
static void pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes);
static void pgsa_unescape_tsv_field(char *str, const char *filename,
unsigned lineno);
static void pgsa_write_entries(pgsa_writer_context *wctx);
pg_noreturn static void pgsa_write_error(pgsa_writer_context *wctx);
static void pgsa_write_stashes(pgsa_writer_context *wctx);
static void pgsa_write_to_disk(void);
/*
* Background worker entry point for pg_stash_advice persistence.
*
* On startup, if stashes_ready is set, we load previously saved
* stash data from disk. Then we enter a loop, periodically checking whether
* any changes have been made (via the change_count atomic counter) and
* writing them to disk. On shutdown, we perform a final write.
*/
PGDLLEXPORT void
pg_stash_advice_worker_main(Datum main_arg)
{
uint64 last_change_count;
TimestampTz last_write_time = 0;
/* Establish signal handlers; once that's done, unblock signals. */
pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
pqsignal(SIGHUP, SignalHandlerForConfigReload);
pqsignal(SIGUSR1, procsignal_sigusr1_handler);
BackgroundWorkerUnblockSignals();
/* Log a debug message */
ereport(DEBUG1,
errmsg("pg_stash_advice worker started"));
/* Set up session user so pgstat can report it. */
InitializeSessionUserIdStandalone();
/* Report this worker in pg_stat_activity. */
pgstat_beinit();
pgstat_bestart_initial();
pgstat_bestart_final();
/* Attach to shared memory structures. */
pgsa_attach();
/* Set on-detach hook so that our PID will be cleared on exit. */
before_shmem_exit(pgsa_detach_shmem, 0);
/*
* Store our PID in shared memory, unless there's already another worker
* running, in which case just exit.
*/
LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
if (pgsa_state->bgworker_pid != InvalidPid)
{
LWLockRelease(&pgsa_state->lock);
ereport(LOG,
(errmsg("pg_stash_advice worker is already running under PID %d",
(int) pgsa_state->bgworker_pid)));
return;
}
pgsa_state->bgworker_pid = MyProcPid;
LWLockRelease(&pgsa_state->lock);
/*
* If pg_stash_advice.persist was set to true during
* process_shared_preload_libraries() and the data has not yet been
* successfully loaded, load it now.
*/
if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready))
{
pgsa_read_from_disk();
pg_atomic_test_set_flag(&pgsa_state->stashes_ready);
}
/* Note the current change count so we can detect future changes. */
last_change_count = pg_atomic_read_u64(&pgsa_state->change_count);
/* Periodically write to disk until terminated. */
while (!ShutdownRequestPending)
{
/* In case of a SIGHUP, just reload the configuration. */
if (ConfigReloadPending)
{
ConfigReloadPending = false;
ProcessConfigFile(PGC_SIGHUP);
}
if (pg_stash_advice_persist_interval <= 0)
{
/* Only writing at shutdown, so just wait forever. */
(void) WaitLatch(MyLatch,
WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
-1L,
PG_WAIT_EXTENSION);
}
else
{
TimestampTz next_write_time;
long delay_in_ms;
uint64 current_change_count;
/* Compute when the next write should happen. */
next_write_time =
TimestampTzPlusMilliseconds(last_write_time,
pg_stash_advice_persist_interval * 1000);
delay_in_ms =
TimestampDifferenceMilliseconds(GetCurrentTimestamp(),
next_write_time);
/*
* When we reach next_write_time, we always update last_write_time
* (which is really the time at which we last considered writing),
* but we only actually write to disk if something has changed.
*/
if (delay_in_ms <= 0)
{
current_change_count =
pg_atomic_read_u64(&pgsa_state->change_count);
if (current_change_count != last_change_count)
{
pgsa_write_to_disk();
last_change_count = current_change_count;
}
last_write_time = GetCurrentTimestamp();
continue;
}
/* Sleep until the next write time. */
(void) WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
delay_in_ms,
PG_WAIT_EXTENSION);
}
ResetLatch(MyLatch);
}
/* Write one last time before exiting. */
pgsa_write_to_disk();
}
/*
* Clear our PID from shared memory on exit.
*/
static void
pgsa_detach_shmem(int code, Datum arg)
{
LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
if (pgsa_state->bgworker_pid == MyProcPid)
pgsa_state->bgworker_pid = InvalidPid;
LWLockRelease(&pgsa_state->lock);
}
/*
* Load advice stash data from a dump file on disk, if there is one.
*/
static void
pgsa_read_from_disk(void)
{
struct stat statbuf;
FILE *file;
char *filebuf;
size_t nread;
char *p;
unsigned lineno;
pgsa_saved_stash_table_hash *saved_stashes;
int num_stashes = 0;
pgsa_saved_entry *entries;
int num_entries = 0;
int max_entries = 64;
MemoryContext tmpcxt;
MemoryContext oldcxt;
Assert(pgsa_entry_dshash != NULL);
/*
* Clear any existing shared memory state.
*
* Normally, there won't be any, but if this function was called before
* and failed after beginning to apply changes to shared memory, then we
* need to get rid of any entries created at that time before trying
* again.
*/
LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
pgsa_reset_all_stashes();
LWLockRelease(&pgsa_state->lock);
/* Open the dump file. If it doesn't exist, we're done. */
file = AllocateFile(PGSA_DUMP_FILE, "r");
if (!file)
{
if (errno == ENOENT)
return;
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not open file \"%s\": %m", PGSA_DUMP_FILE)));
}
/* Use a temporary context for all parse-phase allocations. */
tmpcxt = AllocSetContextCreate(CurrentMemoryContext,
"pg_stash_advice load",
ALLOCSET_DEFAULT_SIZES);
oldcxt = MemoryContextSwitchTo(tmpcxt);
/* Figure out how long the file is. */
if (fstat(fileno(file), &statbuf) != 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not stat file \"%s\": %m", PGSA_DUMP_FILE)));
/*
* Slurp the entire file into memory all at once.
*
* We could avoid this by reading the file incrementally and applying
* changes to pgsa_stash_dshash and pgsa_entry_dshash as we go. Given the
* lockout mechanism implemented by stashes_ready, that shouldn't have any
* user-visible behavioral consequences, but it would consume shared
* memory to no benefit. It seems better to buffer everything in private
* memory first, and then only apply the changes once the file has been
* successfully parsed in its entirety.
*
* That also has the advantage of possibly being more future-proof: if we
* decide to remove the stashes_ready mechanism in the future, or say
* allow for multiple save files, fully validating the file before
* applying any changes will become much more important.
*
* Of course, this approach does have one major disadvantage, which is
* that we'll temporarily use about twice as much memory as we're
* ultimately going to need, but that seems like it shouldn't be a problem
* in practice. If there's so much stashed advice that parsing the disk
* file runs us out of memory, something has gone terribly wrong. In that
* situation, there probably also isn't enough free memory for the
* workload that the advice is attempting to manipulate to run
* successfully.
*/
filebuf = palloc_extended(statbuf.st_size + 1, MCXT_ALLOC_HUGE);
nread = fread(filebuf, 1, statbuf.st_size, file);
if (ferror(file))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not read file \"%s\": %m", PGSA_DUMP_FILE)));
FreeFile(file);
filebuf[nread] = '\0';
/* Initial memory allocations. */
saved_stashes = pgsa_saved_stash_table_create(tmpcxt, 64, NULL);
entries = palloc_array(pgsa_saved_entry, max_entries);
/*
* For memory and CPU efficiency, we parse the file in place. The end of
* each line gets replaced with a NUL byte, and then the end of each field
* within a line gets the same treatment. The advice string is unescaped
* in place, and stash names and query IDs can't contain any special
* characters. All of the resulting pointers point right back into the
* buffer; we only need additional memory to grow the 'entries' array and
* the 'saved_stashes' hash table.
*/
for (p = filebuf, lineno = 1; *p != '\0'; lineno++)
{
char *cursor = p;
char *eol;
char *line_type;
/* Find end of line and NUL-terminate. */
eol = strchr(p, '\n');
if (eol != NULL)
{
*eol = '\0';
p = eol + 1;
if (eol > cursor && eol[-1] == '\r')
eol[-1] = '\0';
}
else
p += strlen(p);
/* Skip empty lines. */
if (*cursor == '\0')
continue;
/* First field is the type of line, either "stash" or "entry". */
line_type = pgsa_next_tsv_field(&cursor);
if (strcmp(line_type, "stash") == 0)
{
char *name;
bool found;
/* Second field should be the stash name. */
name = pgsa_next_tsv_field(&cursor);
if (name == NULL || *name == '\0')
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: expected stash name",
PGSA_DUMP_FILE, lineno)));
/* No further fields are expected. */
if (cursor != NULL)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: expected end of line",
PGSA_DUMP_FILE, lineno)));
/* Reject overlong stash names. */
if (strlen(name) >= NAMEDATALEN)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: stash name too long",
PGSA_DUMP_FILE, lineno)));
/* Duplicate check. */
(void) pgsa_saved_stash_table_insert(saved_stashes, name, &found);
if (found)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: duplicate stash name \"%s\"",
PGSA_DUMP_FILE, lineno, name)));
num_stashes++;
}
else if (strcmp(line_type, "entry") == 0)
{
char *stash_name;
char *queryid_str;
char *advice_str;
char *endptr;
int64 queryId;
/* Second field should be the stash name. */
stash_name = pgsa_next_tsv_field(&cursor);
if (stash_name == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: expected stash name",
PGSA_DUMP_FILE, lineno)));
/* Third field should be the query ID. */
queryid_str = pgsa_next_tsv_field(&cursor);
if (queryid_str == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: expected query ID",
PGSA_DUMP_FILE, lineno)));
/* Fourth field should be the advice string. */
advice_str = pgsa_next_tsv_field(&cursor);
if (advice_str == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: expected advice string",
PGSA_DUMP_FILE, lineno)));
/* No further fields are expected. */
if (cursor != NULL)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: expected end of line",
PGSA_DUMP_FILE, lineno)));
/* Make sure the stash is one we've actually seen. */
if (pgsa_saved_stash_table_lookup(saved_stashes,
stash_name) == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: unknown stash \"%s\"",
PGSA_DUMP_FILE, lineno, stash_name)));
/* Parse the query ID. */
errno = 0;
queryId = strtoi64(queryid_str, &endptr, 10);
if (*endptr != '\0' || errno != 0 || queryid_str == endptr ||
queryId == 0)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: invalid query ID \"%s\"",
PGSA_DUMP_FILE, lineno, queryid_str)));
/* Unescape the advice string. */
pgsa_unescape_tsv_field(advice_str, PGSA_DUMP_FILE, lineno);
/* Append to the entry array. */
if (num_entries >= max_entries)
{
max_entries *= 2;
entries = repalloc_array(entries, pgsa_saved_entry, max_entries);
}
entries[num_entries].stash_name = stash_name;
entries[num_entries].queryId = queryId;
entries[num_entries].advice_string = advice_str;
num_entries++;
}
else
{
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: unrecognized line type",
PGSA_DUMP_FILE, lineno)));
}
}
/*
* Parsing succeeded. Apply everything to shared memory.
*
* At this point, we know that the file we just read is fully valid, but
* it's still possible for this to fail if, for example, DSA memory cannot
* be allocated. If that happens, the worker will die, the postmaster will
* eventually restart it, and we'll try again after clearing any data that
* we did manage to put into shared memory. (Note that we call
* pgsa_reset_all_stashes() at the top of this function.)
*/
pgsa_restore_stashes(saved_stashes);
pgsa_restore_entries(entries, num_entries);
/* Hooray, it worked! Notify the user. */
ereport(LOG,
(errmsg("loaded %d advice stashes and %d entries from \"%s\"",
num_stashes, num_entries, PGSA_DUMP_FILE)));
/* Clean up. */
MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(tmpcxt);
}
/*
* Write all advice stash data to disk.
*
* The file format is a simple TSV with a line-type prefix:
* stash\tstash_name
* entry\tstash_name\tquery_id\tadvice_string
*/
static void
pgsa_write_to_disk(void)
{
pgsa_writer_context wctx = {0};
MemoryContext tmpcxt;
MemoryContext oldcxt;
Assert(pgsa_entry_dshash != NULL);
/* Use a temporary context so all allocations are freed at the end. */
tmpcxt = AllocSetContextCreate(CurrentMemoryContext,
"pg_stash_advice dump",
ALLOCSET_DEFAULT_SIZES);
oldcxt = MemoryContextSwitchTo(tmpcxt);
/* Set up the writer context. */
snprintf(wctx.pathname, MAXPGPATH, "%s.tmp", PGSA_DUMP_FILE);
wctx.file = AllocateFile(wctx.pathname, "w");
if (!wctx.file)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not open file \"%s\": %m", wctx.pathname)));
wctx.nhash = pgsa_stash_name_table_create(tmpcxt, 64, NULL);
initStringInfo(&wctx.buf);
/* Write stash lines, then entry lines. */
pgsa_write_stashes(&wctx);
pgsa_write_entries(&wctx);
/*
* If nothing was written, remove both the temp file and any existing dump
* file rather than installing a zero-length file.
*/
if (wctx.nhash->members == 0)
{
ereport(DEBUG1,
errmsg("there are no advice stashes to save"));
FreeFile(wctx.file);
unlink(wctx.pathname);
if (unlink(PGSA_DUMP_FILE) == 0)
ereport(DEBUG1,
errmsg("removed \"%s\"", PGSA_DUMP_FILE));
}
else
{
if (FreeFile(wctx.file) != 0)
{
int save_errno = errno;
unlink(wctx.pathname);
errno = save_errno;
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not close file \"%s\": %m",
wctx.pathname)));
}
(void) durable_rename(wctx.pathname, PGSA_DUMP_FILE, ERROR);
ereport(LOG,
errmsg("saved %d advice stashes and %d entries to \"%s\"",
(int) wctx.nhash->members, wctx.entries_written,
PGSA_DUMP_FILE));
}
MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(tmpcxt);
}
/*
* Append the TSV-escaped form of str to buf.
*
* Backslash, tab, newline, and carriage return are escaped with backslash
* sequences. All other characters are passed through unchanged.
*/
static void
pgsa_append_tsv_escaped_string(StringInfo buf, const char *str)
{
for (const char *p = str; *p != '\0'; p++)
{
switch (*p)
{
case '\\':
appendStringInfoString(buf, "\\\\");
break;
case '\t':
appendStringInfoString(buf, "\\t");
break;
case '\n':
appendStringInfoString(buf, "\\n");
break;
case '\r':
appendStringInfoString(buf, "\\r");
break;
default:
appendStringInfoChar(buf, *p);
break;
}
}
}
/*
* Extract the next tab-delimited field from *cursor.
*
* Returns a pointer to the field data; when there are no remaining fields,
* returns NULL. For fields other than the last, the tab that terminates the
* field is replaced by \0, so that the caller can always interpret the return
* value as a C string.
*
* When this function is first called for a given line of text, *cursor should
* point to the beginning of the line. On return, *cursor will have been
* advanced past the tab that terminates the current field, or set to NULL if
* we just returned the last field.
*/
static char *
pgsa_next_tsv_field(char **cursor)
{
char *start = *cursor;
char *p;
if (start == NULL)
return NULL;
p = strchr(start, '\t');
if (p != NULL)
{
*p = '\0';
*cursor = p + 1;
}
else
*cursor = NULL;
return start;
}
/*
* Insert entries into shared memory from the parsed entry array.
*/
static void
pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries)
{
LWLockAcquire(&pgsa_state->lock, LW_SHARED);
for (int i = 0; i < num_entries; i++)
{
ereport(DEBUG2,
errmsg("restoring advice stash entry for \"%s\", query ID %" PRId64,
entries[i].stash_name, entries[i].queryId));
pgsa_set_advice_string(entries[i].stash_name,
entries[i].queryId,
entries[i].advice_string);
}
LWLockRelease(&pgsa_state->lock);
}
/*
* Create stashes in shared memory from the parsed stash hash table.
*/
static void
pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes)
{
pgsa_saved_stash_table_iterator iter;
pgsa_saved_stash *s;
LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
pgsa_saved_stash_table_start_iterate(saved_stashes, &iter);
while ((s = pgsa_saved_stash_table_iterate(saved_stashes,
&iter)) != NULL)
{
ereport(DEBUG2,
errmsg("restoring advice stash \"%s\"", s->name));
pgsa_create_stash(s->name);
}
LWLockRelease(&pgsa_state->lock);
}
/*
* Unescape a TSV field in place.
*
* Recognized escape sequences are \\, \t, \n, and \r. A trailing backslash
* or an unrecognized escape sequence is a syntax error.
*/
static void
pgsa_unescape_tsv_field(char *str, const char *filename, unsigned lineno)
{
char *src = str;
char *dst = str;
while (*src != '\0')
{
/* Just pass through anything that's not a backslash-escape. */
if (likely(*src != '\\'))
{
*dst++ = *src++;
continue;
}
/* Check what sort of escape we've got. */
switch (src[1])
{
case '\\':
*dst++ = '\\';
break;
case 't':
*dst++ = '\t';
break;
case 'n':
*dst++ = '\n';
break;
case 'r':
*dst++ = '\r';
break;
case '\0':
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: trailing backslash",
filename, lineno)));
break;
default:
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("syntax error in file \"%s\" line %u: unrecognized escape \"\\%c\"",
filename, lineno, src[1])));
break;
}
/* We consumed the backslash and the following character. */
src += 2;
}
*dst = '\0';
}
/*
* Write an entry line for each advice entry.
*/
static void
pgsa_write_entries(pgsa_writer_context *wctx)
{
dshash_seq_status iter;
pgsa_entry *entry;
dshash_seq_init(&iter, pgsa_entry_dshash, false);
while ((entry = dshash_seq_next(&iter)) != NULL)
{
pgsa_stash_name *n;
char *advice_string;
if (entry->advice_string == InvalidDsaPointer)
continue;
n = pgsa_stash_name_table_lookup(wctx->nhash,
entry->key.pgsa_stash_id);
if (n == NULL)
continue; /* orphan entry, skip */
advice_string = dsa_get_address(pgsa_dsa_area, entry->advice_string);
resetStringInfo(&wctx->buf);
appendStringInfo(&wctx->buf, "entry\t%s\t%" PRId64 "\t",
n->name, entry->key.queryId);
pgsa_append_tsv_escaped_string(&wctx->buf, advice_string);
appendStringInfoChar(&wctx->buf, '\n');
fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file);
if (ferror(wctx->file))
pgsa_write_error(wctx);
wctx->entries_written++;
}
dshash_seq_term(&iter);
}
/*
* Clean up and report a write error. Does not return.
*/
static void
pgsa_write_error(pgsa_writer_context *wctx)
{
int save_errno = errno;
FreeFile(wctx->file);
unlink(wctx->pathname);
errno = save_errno;
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to file \"%s\": %m", wctx->pathname)));
}
/*
* Write a stash line for each advice stash, and populate the ID-to-name
* hash table for use by pgsa_write_entries.
*/
static void
pgsa_write_stashes(pgsa_writer_context *wctx)
{
dshash_seq_status iter;
pgsa_stash *stash;
dshash_seq_init(&iter, pgsa_stash_dshash, false);
while ((stash = dshash_seq_next(&iter)) != NULL)
{
pgsa_stash_name *n;
bool found;
n = pgsa_stash_name_table_insert(wctx->nhash, stash->pgsa_stash_id,
&found);
Assert(!found);
n->name = pstrdup(stash->name);
resetStringInfo(&wctx->buf);
appendStringInfo(&wctx->buf, "stash\t%s\n", n->name);
fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file);
if (ferror(wctx->file))
pgsa_write_error(wctx);
}
dshash_seq_term(&iter);
}