From 13d47451a0532d123bdac4afb7b18a2c045ba441 Mon Sep 17 00:00:00 2001
From: Yura Sokolov <y.sokolov@postgrespro.ru>
Date: Fri, 27 Mar 2026 20:55:24 +0300
Subject: [PATCH v2 2/3] bufmgr: Add test for BM_IO_ERROR presence after write
 error in FlushBuffer

smgrwrite should raise error on mdwrite error. Lets immitate it with
injection point.
Check BM_IO_ERROR is set and second attempt leads to "Multiple failures"
warning.
---
 src/backend/storage/buffer/bufmgr.c         |  2 +
 src/test/modules/test_aio/t/001_aio.pl      | 27 ++++++++++
 src/test/modules/test_aio/test_aio--1.0.sql | 11 +++-
 src/test/modules/test_aio/test_aio.c        | 58 +++++++++++++++++++--
 4 files changed, 94 insertions(+), 4 deletions(-)

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index a81949aca7c..fd12566e674 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -64,6 +64,7 @@
 #include "storage/read_stream.h"
 #include "storage/smgr.h"
 #include "storage/standby.h"
+#include "utils/injection_point.h"
 #include "utils/memdebug.h"
 #include "utils/ps_status.h"
 #include "utils/rel.h"
@@ -4560,6 +4561,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 			  bufBlock,
 			  false);
 
+	INJECTION_POINT("flush-buffer-after-smgr-write", buf);
 	/*
 	 * When a strategy is in use, only flushes of dirty buffers already in the
 	 * strategy ring are counted as strategy writes (IOCONTEXT
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index a48b0ddfba8..54061e0425b 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -818,6 +818,33 @@ SELECT invalidate_rel_block('tbl_ok', 2);
 	);
 	$psql->query_safe(qq(SELECT inj_io_short_read_detach()));
 
+	# Now test write error
+	$psql->query_safe("SELECT count(*) FROM tbl_ok");
+	$psql->query_safe("SELECT inj_io_error_flush_buffer_attach();");
+	psql_like(
+		$io_method, $psql,
+		"flush error is reported",
+		qq(SELECT invalidate_rel_block('tbl_ok', 2, force_flush=>true)),
+		qr/^$/,
+		qr/ERROR:.*injection point triggering failure to flush buffer/
+	);
+	# BM_IO_ERROR, BM_VALID and BM_TAG_VALID should be set
+	psql_like(
+		$io_method, $psql,
+		"validate flags of 'tbl_ok' page after read_rel_block_ll()",
+		qq(SELECT debug_print_rel_block('tbl_ok', 2)),
+		qr/blockNum=2, flags=0x.?b.00000, refcount=0/, qr/^$/);
+	# Validate second attempt leads to "Multiple failures" message
+	psql_like(
+		$io_method, $psql,
+		"flush error is reported",
+		qq(SELECT invalidate_rel_block('tbl_ok', 2, force_flush=>true)),
+		qr/^$/,
+		qr/Multiple failures --- write error might be permanent/
+	);
+
+	$psql->query_safe(qq(SELECT inj_io_error_flush_buffer_detach()));
+
 	$psql->quit();
 }
 
diff --git a/src/test/modules/test_aio/test_aio--1.0.sql b/src/test/modules/test_aio/test_aio--1.0.sql
index cb168e2e08f..51af9a74b13 100644
--- a/src/test/modules/test_aio/test_aio--1.0.sql
+++ b/src/test/modules/test_aio/test_aio--1.0.sql
@@ -41,7 +41,8 @@ CREATE FUNCTION debug_print_rel_block(rel regclass, blockno int)
 RETURNS pg_catalog.text STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
 
-CREATE FUNCTION invalidate_rel_block(rel regclass, blockno int)
+CREATE FUNCTION invalidate_rel_block(rel regclass, blockno int,
+    force_flush bool DEFAULT false)
 RETURNS pg_catalog.void STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
 
@@ -132,3 +133,11 @@ AS 'MODULE_PATHNAME' LANGUAGE C;
 CREATE FUNCTION inj_io_reopen_detach()
 RETURNS pg_catalog.void STRICT
 AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION inj_io_error_flush_buffer_attach()
+RETURNS pg_catalog.void STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION inj_io_error_flush_buffer_detach()
+RETURNS pg_catalog.void STRICT
+AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index a89cbd5786d..297800107a6 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -50,6 +50,7 @@ typedef struct InjIoErrorState
 
 	bool		enabled_short_read;
 	bool		enabled_reopen;
+	bool		enabled_error_flush_buffer;
 
 	bool		enabled_completion_wait;
 	Oid			completion_wait_relfilenode;
@@ -131,6 +132,12 @@ test_aio_shmem_startup(void)
 							 0);
 		InjectionPointLoad("aio-worker-after-reopen");
 
+		InjectionPointAttach("flush-buffer-after-smgr-write",
+							 "test_aio",
+							 "inj_io_error_flush_buffer",
+							 NULL,
+							 0);
+		InjectionPointLoad("flush-buffer-after-smgrwrite");
 #endif
 	}
 	else
@@ -142,6 +149,7 @@ test_aio_shmem_startup(void)
 #ifdef USE_INJECTION_POINTS
 		InjectionPointLoad("aio-process-completion-before-shared");
 		InjectionPointLoad("aio-worker-after-reopen");
+		InjectionPointLoad("flush-buffer-after-smgr-write");
 		elog(LOG, "injection point loaded");
 #endif
 	}
@@ -489,7 +497,7 @@ read_rel_block_ll(PG_FUNCTION_ARGS)
 
 /* helper for invalidate_rel_block() and evict_rel() */
 static void
-invalidate_one_block(Relation rel, ForkNumber forknum, BlockNumber blkno)
+invalidate_one_block(Relation rel, ForkNumber forknum, BlockNumber blkno, bool force_flush)
 {
 	PrefetchBufferResult pr;
 	Buffer		buf;
@@ -513,6 +521,9 @@ invalidate_one_block(Relation rel, ForkNumber forknum, BlockNumber blkno)
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
+			if (force_flush)
+				MarkBufferDirty(buf);
+
 			if (pg_atomic_read_u64(&buf_hdr->state) & BM_DIRTY)
 			{
 				if (BufferIsLocal(buf))
@@ -537,11 +548,12 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 {
 	Oid			relid = PG_GETARG_OID(0);
 	BlockNumber blkno = PG_GETARG_UINT32(1);
+	bool		force_flush = PG_GETARG_BOOL(2);
 	Relation	rel;
 
 	rel = relation_open(relid, AccessExclusiveLock);
 
-	invalidate_one_block(rel, MAIN_FORKNUM, blkno);
+	invalidate_one_block(rel, MAIN_FORKNUM, blkno, force_flush);
 
 	relation_close(rel, AccessExclusiveLock);
 
@@ -577,7 +589,7 @@ evict_rel(PG_FUNCTION_ARGS)
 
 			for (int blkno = 0; blkno < nblocks; blkno++)
 			{
-				invalidate_one_block(rel, forknum, blkno);
+				invalidate_one_block(rel, forknum, blkno, false);
 			}
 		}
 	}
@@ -1035,6 +1047,9 @@ extern PGDLLEXPORT void inj_io_completion_hook(const char *name,
 extern PGDLLEXPORT void inj_io_reopen(const char *name,
 									  const void *private_data,
 									  void *arg);
+extern PGDLLEXPORT void inj_io_error_flush_buffer(const char *name,
+												  const void *private_data,
+												  void *arg);
 
 static bool
 inj_io_short_read_matches(PgAioHandle *ioh)
@@ -1203,6 +1218,18 @@ inj_io_reopen(const char *name, const void *private_data, void *arg)
 	if (inj_io_error_state->enabled_reopen)
 		elog(ERROR, "injection point triggering failure to reopen ");
 }
+
+void
+inj_io_error_flush_buffer(const char *name, const void *private_data, void *arg)
+{
+	ereport(LOG,
+			errmsg("error_flush_buffer injection point called, is enabled: %d",
+				   inj_io_error_state->enabled_error_flush_buffer),
+			errhidestmt(true), errhidecontext(true));
+
+	if (inj_io_error_state->enabled_error_flush_buffer)
+		elog(ERROR, "injection point triggering failure to flush buffer ");
+}
 #endif
 
 PG_FUNCTION_INFO_V1(inj_io_completion_wait);
@@ -1297,3 +1324,28 @@ inj_io_reopen_detach(PG_FUNCTION_ARGS)
 #endif
 	PG_RETURN_VOID();
 }
+
+PG_FUNCTION_INFO_V1(inj_io_error_flush_buffer_attach);
+Datum
+inj_io_error_flush_buffer_attach(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	inj_io_error_state->enabled_error_flush_buffer = true;
+#else
+	elog(ERROR, "injection points not supported");
+#endif
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(inj_io_error_flush_buffer_detach);
+Datum
+inj_io_error_flush_buffer_detach(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	inj_io_error_state->enabled_error_flush_buffer = false;
+#else
+	elog(ERROR, "injection points not supported");
+#endif
+	PG_RETURN_VOID();
+}
-- 
2.51.0

