Hi,

There is another thread [1] to add both pg_buffercache_evict_[relation
| all] and pg_buffercache_mark_dirty[_all] functions to the
pg_buffercache. I decided to create another thread as
pg_buffercache_evict_[relation | all] functions are committed but
pg_buffercache_mark_dirty[_all] functions still need review.

pg_buffercache_mark_dirty(): This function takes a buffer id as an
argument and tries to mark this buffer as dirty. Returns true on
success.
pg_buffercache_mark_dirty_all(): This is very similar to the
pg_buffercache_mark_dirty() function. The difference is
pg_buffercache_mark_dirty_all() does not take an argument. Instead it
just loops over the shared buffers and tries to mark all of them as
dirty. It returns the number of buffers marked as dirty.

Since that patch is targeted for the PG 19, pg_buffercache is bumped to v1.7.

Latest version is attached and people who already reviewed the patches are CCed.

[1] 
postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com

--
Regards,
Nazir Bilal Yavuz
Microsoft
From 06f12f6174c0b6e1c5beeb4c1a8f4b33b89cc158 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavu...@gmail.com>
Date: Fri, 4 Apr 2025 13:39:49 +0300
Subject: [PATCH v7] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.

Author: Nazir Bilal Yavuz <byavu...@gmail.com>
Reviewed-by: Andres Freund <and...@anarazel.de>
Reviewed-by: Aidar Imamov <a.ima...@postgrespro.ru>
Reviewed-by: Joseph Koshakow <kosh...@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |  2 +
 src/backend/storage/buffer/bufmgr.c           | 75 +++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 54 ++++++++++++-
 contrib/pg_buffercache/Makefile               |  2 +-
 .../expected/pg_buffercache.out               | 30 +++++++-
 contrib/pg_buffercache/meson.build            |  1 +
 .../pg_buffercache--1.6--1.7.sql              | 14 ++++
 contrib/pg_buffercache/pg_buffercache.control |  2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 33 ++++++++
 contrib/pg_buffercache/sql/pg_buffercache.sql | 10 ++-
 10 files changed, 216 insertions(+), 7 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 33a8b8c06fb..ec7fec6368a 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -312,6 +312,8 @@ extern void EvictRelUnpinnedBuffers(Relation rel,
 									int32 *buffers_evicted,
 									int32 *buffers_flushed,
 									int32 *buffers_skipped);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
+extern void MarkAllUnpinnedBuffersDirty(int32 *buffers_dirtied);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index db8f2b1754e..e2bdb86525b 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6699,6 +6699,81 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 	}
 }
 
+/*
+ * Try to mark the provided shared buffer as dirty.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside.
+ *
+ * Returns true if the buffer was already dirty or it has successfully been
+ * marked as dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+
+	UnpinBuffer(desc);
+
+	return true;
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkUnpinnedBufferDirty().
+ *
+ * The buffers_dirtied parameter is mandatory and indicate the total count of
+ * buffers were dirtied.
+ */
+void
+MarkAllUnpinnedBuffersDirty(int32 *buffers_dirtied)
+{
+	*buffers_dirtied = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			(*buffers_dirtied)++;
+	}
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 537d6014942..bfccabd9c5e 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -35,6 +35,14 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
@@ -42,9 +50,11 @@
   <structname>pg_buffercache_numa</structname> view), the
   <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
-  <function>pg_buffercache_evict()</function>, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict()</function> function, the
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -99,6 +109,18 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -522,6 +544,32 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a
+   buffer identifier, as shown in the <structfield>bufferid</structfield>
+   column of the <structname>pg_buffercache</structname> view.  It returns
+   true on success, and false if the buffer wasn't valid or if it couldn't be
+   marked as dirty because it was pinned.  The result is immediately out of
+   date upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very
+   similar to the <function>pg_buffercache_mark_dirty()</function> function.
+   The difference is, the <function>pg_buffercache_mark_dirty_all()</function>
+   function does not take an argument; instead it tries to mark all buffers
+   dirty in the buffer pool.  The result is immediately out of date upon
+   return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index 5f748543e2e..0e618f66aec 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -9,7 +9,7 @@ EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
 	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
-	pg_buffercache--1.5--1.6.sql
+	pg_buffercache--1.5--1.6.sql pg_buffercache--1.6--1.7.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache pg_buffercache_numa
diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index 9a9216dc7b1..aa2c0328386 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -57,7 +57,7 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
 
 RESET role;
 ------
----- Test pg_buffercache_evict* functions
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
 ------
 CREATE ROLE regress_buffercache_normal;
 SET ROLE regress_buffercache_normal;
@@ -68,6 +68,10 @@ SELECT * FROM pg_buffercache_evict_relation(1);
 ERROR:  must be superuser to use pg_buffercache_evict_relation()
 SELECT * FROM pg_buffercache_evict_all();
 ERROR:  must be superuser to use pg_buffercache_evict_all()
+SELECT * FROM pg_buffercache_mark_dirty(1);
+ERROR:  must be superuser to use pg_buffercache_mark_dirty()
+SELECT * FROM pg_buffercache_mark_dirty_all();
+ERROR:  must be superuser to use pg_buffercache_mark_dirty_all()
 RESET ROLE;
 -- These should return nothing, because these are STRICT functions
 SELECT * FROM pg_buffercache_evict(NULL);
@@ -82,6 +86,12 @@ SELECT * FROM pg_buffercache_evict_relation(NULL);
                  |                 |                
 (1 row)
 
+SELECT * FROM pg_buffercache_mark_dirty(NULL);
+ pg_buffercache_mark_dirty 
+---------------------------
+ 
+(1 row)
+
 -- These should fail because they are not called by valid range of buffers
 -- Number of the shared buffers are limited by max integer
 SELECT 2147483647 max_buffers \gset
@@ -91,6 +101,12 @@ SELECT * FROM pg_buffercache_evict(0);
 ERROR:  bad buffer ID: 0
 SELECT * FROM pg_buffercache_evict(:max_buffers);
 ERROR:  bad buffer ID: 2147483647
+SELECT * FROM pg_buffercache_mark_dirty(-1);
+ERROR:  bad buffer ID: -1
+SELECT * FROM pg_buffercache_mark_dirty(0);
+ERROR:  bad buffer ID: 0
+SELECT * FROM pg_buffercache_mark_dirty(:max_buffers);
+ERROR:  bad buffer ID: 2147483647
 -- This should fail because pg_buffercache_evict_relation() doesn't accept
 -- local relations
 CREATE TEMP TABLE temp_pg_buffercache();
@@ -118,4 +134,16 @@ SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg
 (1 row)
 
 DROP TABLE shared_pg_buffercache;
+SELECT pg_buffercache_mark_dirty(1) IS NOT NULL;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT pg_buffercache_mark_dirty_all() IS NOT NULL;
+ ?column? 
+----------
+ t
+(1 row)
+
 DROP ROLE regress_buffercache_normal;
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 7cd039a1df9..7c31141881f 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -24,6 +24,7 @@ install_data(
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
   'pg_buffercache--1.5--1.6.sql',
+  'pg_buffercache--1.6--1.7.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
new file mode 100644
index 00000000000..db55c38e9b9
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
@@ -0,0 +1,14 @@
+/* contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.7'" to load this file. \quit
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index b030ba3a6fa..11499550945 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.6'
+default_version = '1.7'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index e1701bd56ef..6136ab3a0ae 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -100,6 +100,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -772,3 +774,34 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark a shared buffer as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty");
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_all");
+
+	MarkAllUnpinnedBuffersDirty(&buffers_dirtied);
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 47cca1907c7..9eac5214825 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -30,7 +30,7 @@ RESET role;
 
 
 ------
----- Test pg_buffercache_evict* functions
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
 ------
 
 CREATE ROLE regress_buffercache_normal;
@@ -40,12 +40,15 @@ SET ROLE regress_buffercache_normal;
 SELECT * FROM pg_buffercache_evict(1);
 SELECT * FROM pg_buffercache_evict_relation(1);
 SELECT * FROM pg_buffercache_evict_all();
+SELECT * FROM pg_buffercache_mark_dirty(1);
+SELECT * FROM pg_buffercache_mark_dirty_all();
 
 RESET ROLE;
 
 -- These should return nothing, because these are STRICT functions
 SELECT * FROM pg_buffercache_evict(NULL);
 SELECT * FROM pg_buffercache_evict_relation(NULL);
+SELECT * FROM pg_buffercache_mark_dirty(NULL);
 
 -- These should fail because they are not called by valid range of buffers
 -- Number of the shared buffers are limited by max integer
@@ -53,6 +56,9 @@ SELECT 2147483647 max_buffers \gset
 SELECT * FROM pg_buffercache_evict(-1);
 SELECT * FROM pg_buffercache_evict(0);
 SELECT * FROM pg_buffercache_evict(:max_buffers);
+SELECT * FROM pg_buffercache_mark_dirty(-1);
+SELECT * FROM pg_buffercache_mark_dirty(0);
+SELECT * FROM pg_buffercache_mark_dirty(:max_buffers);
 
 -- This should fail because pg_buffercache_evict_relation() doesn't accept
 -- local relations
@@ -66,5 +72,7 @@ SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_all();
 CREATE TABLE shared_pg_buffercache();
 SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg_buffercache');
 DROP TABLE shared_pg_buffercache;
+SELECT pg_buffercache_mark_dirty(1) IS NOT NULL;
+SELECT pg_buffercache_mark_dirty_all() IS NOT NULL;
 
 DROP ROLE regress_buffercache_normal;
-- 
2.49.0

Reply via email to