From e69efed3b1a6334b8c80982c02ed177374539a64 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Mon, 13 Jul 2026 14:25:25 -0500
Subject: [PATCH v2 3/3] pgstat: change to_serialized_data callback to return
 bool

Change the to_serialized_data callback signature from void to bool,
making it symmetric with from_serialized_data.  This allows extensions
to signal write failure back to pgstat_write_statsfile().

When the callback returns false, pgstat_write_statsfile() discards the
temporary stats file, preventing the core stats file from being written
to disk when there is missing auxiliary data. The core stats file would
have been discarded on the next startup anyhow.

Update test_custom_stats to check write_chunk() return values and
propagate failure through the new bool return.
---
 src/backend/utils/activity/pgstat.c           | 20 +++++++++++--
 src/include/utils/pgstat_internal.h           |  6 ++--
 .../test_custom_stats/test_custom_var_stats.c | 29 ++++++++++++-------
 3 files changed, 39 insertions(+), 16 deletions(-)

diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index b89c21da6ee..9234854b8b5 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -1776,8 +1776,12 @@ pgstat_write_statsfile(void)
 					pgstat_get_entry_len(ps->key.kind));
 
 		/* Write more data for the entry, if required */
-		if (kind_info->to_serialized_data)
-			kind_info->to_serialized_data(&ps->key, shstats, fpout);
+		if (kind_info->to_serialized_data &&
+			!kind_info->to_serialized_data(&ps->key, shstats, fpout))
+		{
+			status = STATS_DISCARD;
+			break;
+		}
 	}
 	dshash_seq_term(&hstat);
 
@@ -1788,7 +1792,17 @@ pgstat_write_statsfile(void)
 	 */
 	fputc(PGSTAT_FILE_ENTRY_END, fpout);
 
-	if (ferror(fpout))
+	if (status == STATS_DISCARD)
+	{
+		/*
+		 * A to_serialized_data callback failed.  DEBUG2 because the callback
+		 * already logged the reason.
+		 */
+		elog(DEBUG2, "discarding temporary statistics file \"%s\"", tmpfile);
+		FreeFile(fpout);
+		unlink(tmpfile);
+	}
+	else if (ferror(fpout))
 	{
 		ereport(LOG,
 				(errcode_for_file_access(),
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 02022f19b81..1c03e797cbb 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -322,7 +322,9 @@ typedef struct PgStat_KindInfo
 	 * an entry, in the stats file or optionally in a different file.
 	 * Optional.
 	 *
-	 * to_serialized_data: write auxiliary data for an entry.
+	 * to_serialized_data: write auxiliary data for an entry.  Returns true on
+	 * success, false on write error.  When false is returned,
+	 * pgstat_write_statsfile() will discard the stats file.
 	 *
 	 * from_serialized_data: read auxiliary data for an entry.  Returns true
 	 * on success, false on read error.
@@ -332,7 +334,7 @@ typedef struct PgStat_KindInfo
 	 * just written or read.  "header" is a pointer to the stats data; it may
 	 * be modified only in from_serialized_data to reconstruct an entry.
 	 */
-	void		(*to_serialized_data) (const PgStat_HashKey *key,
+	bool		(*to_serialized_data) (const PgStat_HashKey *key,
 									   const PgStatShared_Common *header,
 									   FILE *statfile);
 	bool		(*from_serialized_data) (const PgStat_HashKey *key,
diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c
index 34d474be604..a39ada0b67c 100644
--- a/src/test/modules/test_custom_stats/test_custom_var_stats.c
+++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c
@@ -26,7 +26,7 @@ PG_MODULE_MAGIC_EXT(
 );
 
 /* Local helpers for stats file I/O */
-#define write_chunk(fpout, ptr, len) ((void) fwrite(ptr, len, 1, fpout))
+#define write_chunk(fpout, ptr, len) (fwrite(ptr, len, 1, fpout) == 1)
 #define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr))
 #define read_chunk(fpin, ptr, len) (fread(ptr, 1, len, fpin) == (len))
 #define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr))
@@ -94,7 +94,7 @@ static bool test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref,
 												   bool nowait);
 
 /* Serialization callback: write auxiliary entry data */
-static void test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
+static bool test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 													 const PgStatShared_Common *header,
 													 FILE *statfile);
 
@@ -193,7 +193,7 @@ test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, bool nowait)
  * - The length of the description.
  * - The description data itself.
  */
-static void
+static bool
 test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 										 const PgStatShared_Common *header,
 										 FILE *statfile)
@@ -208,7 +208,8 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	 * First mark the main file with a magic number, keeping a trace that some
 	 * auxiliary data will exist in the secondary statistics file.
 	 */
-	write_chunk_s(statfile, &magic_number);
+	if (!write_chunk_s(statfile, &magic_number))
+		return false;
 
 	/* Open statistics file for writing. */
 	if (!fd_description)
@@ -220,7 +221,7 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 					(errcode_for_file_access(),
 					 errmsg("could not open statistics file \"%s\" for writing: %m",
 							TEST_CUSTOM_AUX_DATA_DESC)));
-			return;
+			return false;
 		}
 
 		/* Initialize offset for secondary statistics file. */
@@ -228,14 +229,16 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	}
 
 	/* Write offset to the main data file */
-	write_chunk_s(statfile, &fd_description_offset);
+	if (!write_chunk_s(statfile, &fd_description_offset))
+		return false;
 
 	/*
 	 * First write the entry key to the secondary statistics file.  This will
 	 * be cross-checked with the key read from main stats file at loading
 	 * time.
 	 */
-	write_chunk_s(fd_description, (PgStat_HashKey *) key);
+	if (!write_chunk_s(fd_description, (PgStat_HashKey *) key))
+		return false;
 	fd_description_offset += sizeof(PgStat_HashKey);
 
 	if (!custom_stats_description_dsa)
@@ -246,9 +249,10 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	{
 		/* length to description file */
 		len = 0;
-		write_chunk_s(fd_description, &len);
+		if (!write_chunk_s(fd_description, &len))
+			return false;
 		fd_description_offset += sizeof(size_t);
-		return;
+		return true;
 	}
 
 	/*
@@ -258,14 +262,17 @@ test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key,
 	description = dsa_get_address(custom_stats_description_dsa,
 								  entry->description);
 	len = strlen(description) + 1;
-	write_chunk_s(fd_description, &len);
-	write_chunk(fd_description, description, len);
+	if (!write_chunk_s(fd_description, &len))
+		return false;
+	if (!write_chunk(fd_description, description, len))
+		return false;
 
 	/*
 	 * Update offset for next entry, counting for the length (size_t) of the
 	 * description and the description contents.
 	 */
 	fd_description_offset += len + sizeof(size_t);
+	return true;
 }
 
 /*
-- 
2.50.1 (Apple Git-155)

