Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Started by Nazir Bilal Yavuz9 months ago21 messages
#1Nazir Bilal Yavuz
byavuz81@gmail.com
1 attachment(s)

Hi,

There is another thread [1]postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com 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

Attachments:

v7-0001-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 06f12f6174c0b6e1c5beeb4c1a8f4b33b89cc158 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@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 <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@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

#2Xuneng Zhou
xunengzhou@gmail.com
In reply to: Nazir Bilal Yavuz (#1)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,
I’ve been trying to review this patch, and it struck me that we’re
currently grabbing the content lock exclusively just to flip a header bit:

if (!(buf_state & BM_DIRTY))
{
LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
MarkBufferDirty(buf);
LWLockRelease(BufferDescriptorGetContentLock(desc));
}

Since our sole goal here is to simulate a buffer’s dirty state for
testing/debugging, I wonder—could we instead:

1. Acquire the shared content lock (which already blocks eviction),
2. Call MarkBufferDirtyHint() to flip the BM_DIRTY bit under the header
spinlock, and
3. Release the shared lock?

This seems to satisfy Assert(LWLockHeldByMe(...)) inside the hint routine
and would:

- Prevent exclusive‐lock contention when sweeping many buffers,
- Avoid full‐page WAL writes (we only need a hint record, if any)

Would love to hear if this makes sense or or am I overlooking something
here. Thanks for any feedback!

Cheers,
Xuneng

Nazir Bilal Yavuz <byavuz81@gmail.com> 于2025年4月11日周五 17:14写道:

Show quoted text

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

#3Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Xuneng Zhou (#2)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

On Fri, 25 Apr 2025 at 19:17, Xuneng Zhou <xunengzhou@gmail.com> wrote:

Hi,
I’ve been trying to review this patch, and it struck me that we’re currently grabbing the content lock exclusively just to flip a header bit:

Thank you for looking into this!

if (!(buf_state & BM_DIRTY))
{
LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
MarkBufferDirty(buf);
LWLockRelease(BufferDescriptorGetContentLock(desc));
}

Since our sole goal here is to simulate a buffer’s dirty state for testing/debugging, I wonder—could we instead:

I believe our goal is not only to simulate a buffer’s dirty state but
also replicating steps to mark the buffers as dirty.

1. Acquire the shared content lock (which already blocks eviction),
2. Call MarkBufferDirtyHint() to flip the BM_DIRTY bit under the header spinlock, and
3. Release the shared lock?

This seems to satisfy Assert(LWLockHeldByMe(...)) inside the hint routine and would:

- Prevent exclusive‐lock contention when sweeping many buffers,
- Avoid full‐page WAL writes (we only need a hint record, if any)

Would love to hear if this makes sense or or am I overlooking something here. Thanks for any feedback!

I think what you said makes sense and is correct if we only want to
simulate a buffer’s dirty state for testing/debugging, but if we want
to replicate usual steps to marking buffers as dirty, then I think we
need to have full-page WAL writes.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#4Amit Kapila
amit.kapila16@gmail.com
In reply to: Nazir Bilal Yavuz (#3)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

On Mon, Apr 28, 2025 at 2:43 PM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote:

Hi,

On Fri, 25 Apr 2025 at 19:17, Xuneng Zhou <xunengzhou@gmail.com> wrote:

Would love to hear if this makes sense or or am I overlooking something here. Thanks for any feedback!

I think what you said makes sense and is correct if we only want to
simulate a buffer’s dirty state for testing/debugging, but if we want
to replicate usual steps to marking buffers as dirty, then I think we
need to have full-page WAL writes.

Fair enough. But you haven't mentioned how exactly you want to use
these functions for testing? That will help us to understand whether
we need to replicate all the steps to mark the buffer dirty.

Also, I feel it will be easier for one to test the functionality by
marking buffers dirty for a particular relation rather than by using
buffer_id but maybe I am missing the testing scenarios you have in
mind for the proposed APIs.

The other point to consider was whether we need to lock the relation
for the proposed functions. If we already mark buffers dirty by
scanning the buffer pool in bgwriter/checkpointer without acquiring a
lock on the relation, then why do we need it here?

--
With Regards,
Amit Kapila.

#5Xuneng Zhou
xunengzhou@gmail.com
In reply to: Nazir Bilal Yavuz (#1)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hey,

I noticed a couple of small clarity issues in the current version of patch
for potential clean up:

1. Commit message wording

Right now it says:

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

That makes it sound like the *_all* function is the granular one, when
really:

• pg_buffercache_mark_dirty(buffernumber) is the fine-grained, per-buffer
call.

• pg_buffercache_mark_dirty_all() is the bulk, coarse-grained operation.

How about rephrasing to:

“The pg_buffercache_mark_dirty_all() function provides an efficient, bulk
way to mark every buffer dirty (e.g., ~70 ms vs. ~550 ms for 16 GB of
shared buffers), while pg_buffercache_mark_dirty() still allows per-buffer,
granular control.”

2. Inline comment in MarkUnpinnedBufferDirty

We currently have:

PinBuffer_Locked(desc); */* releases spinlock */*

Folks who’re unfamiliar with this function might get confused. Maybe we
could use the one in GetVictimBuffer:

*/* Pin the buffer and then release its spinlock */*

PinBuffer_Locked(buf_hdr);

That spelling-out makes it obvious what’s happening.

Show quoted text

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.

#6Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Amit Kapila (#4)
1 attachment(s)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

Thank you for looking into this! And sorry for the late reply.

On Mon, 12 May 2025 at 13:24, Amit Kapila <amit.kapila16@gmail.com> wrote:

On Mon, Apr 28, 2025 at 2:43 PM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote:

Hi,

On Fri, 25 Apr 2025 at 19:17, Xuneng Zhou <xunengzhou@gmail.com> wrote:

Would love to hear if this makes sense or or am I overlooking something here. Thanks for any feedback!

I think what you said makes sense and is correct if we only want to
simulate a buffer’s dirty state for testing/debugging, but if we want
to replicate usual steps to marking buffers as dirty, then I think we
need to have full-page WAL writes.

Fair enough. But you haven't mentioned how exactly you want to use
these functions for testing? That will help us to understand whether
we need to replicate all the steps to mark the buffer dirty.

Sorry, you are right. Main idea of this change is to test WAL writes
on CHECKPOINT, I think MarkBufferDirtyHint() is enough for this
purpose but I was thinking about someone else might want to use this
function and I think they expect to replicate all steps to mark the
buffer dirty.

Also, I feel it will be easier for one to test the functionality by
marking buffers dirty for a particular relation rather than by using
buffer_id but maybe I am missing the testing scenarios you have in
mind for the proposed APIs.

This is done in the v8.

The other point to consider was whether we need to lock the relation
for the proposed functions. If we already mark buffers dirty by
scanning the buffer pool in bgwriter/checkpointer without acquiring a
lock on the relation, then why do we need it here?

Sorry, I couldn't find this code part. Could you please elaborate more?

I updated patch a bit more, summary is:

- pg_buffercache_mark_dirty_relation() is added in v8.

- Function call is very similar to pg_buffercache_evict*(), we have
MarkDirtyUnpinnedBufferInternal() function inside bufmgr.c file and
this function is called from other functions.

-
pg_buffercache_mark_dirty() returns -> buffer_dirtied and
buffer_already_dirty as a boolean
pg_buffercache_mark_dirty_relation() returns -> buffers_dirtied,
buffers_already_dirty and buffers_skipped
pg_buffercache_mark_dirty_all() returns -> buffers_dirtied,
buffers_already_dirty and buffers_skipped

- Commit message and docs are updated regarding the changes above.

v8 is attached.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v8-0001-Add-pg_buffercache_mark_dirty-_relation-_all-func.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Add-pg_buffercache_mark_dirty-_relation-_all-func.patchDownload
From 5a850eb46467284b580ba9ea612eb6cff112383a Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:39:49 +0300
Subject: [PATCH v8] Add pg_buffercache_mark_dirty{,_relation,_all}() functions

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

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirt_relation(): Marks all shared buffers as dirty
in a relation at once.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty at
once.

The pg_buffercache_mark_dirty_relation() and
pg_buffercache_mark_dirty_all() functions provide mechanism to mark
multiple shared buffers as dirty at once. They are designed to address
the inefficiency of repeatedly calling
pg_buffercache_mark_dirty() for each individual buffer, which can be
time-consuming when dealing with with large shared buffers pool. (e.g.,
~550ms vs. ~70ms for 16GB of fully populated shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Minimal tests for the new functions are included.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Xuneng Zhou <xunengzhou@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |   8 +
 src/backend/storage/buffer/bufmgr.c           | 188 ++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               |  91 ++++++++-
 contrib/pg_buffercache/Makefile               |   2 +-
 .../expected/pg_buffercache.out               |  49 ++++-
 contrib/pg_buffercache/meson.build            |   1 +
 .../pg_buffercache--1.6--1.7.sql              |  26 +++
 contrib/pg_buffercache/pg_buffercache.control |   2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 123 ++++++++++++
 contrib/pg_buffercache/sql/pg_buffercache.sql |  17 +-
 10 files changed, 497 insertions(+), 10 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 41fdc1e7693..148d4024f16 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -315,6 +315,14 @@ extern void EvictRelUnpinnedBuffers(Relation rel,
 									int32 *buffers_evicted,
 									int32 *buffers_flushed,
 									int32 *buffers_skipped);
+extern bool MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty);
+extern void MarkDirtyRelUnpinnedBuffers(Relation rel,
+										int32 *buffers_dirtied,
+										int32 *buffers_already_dirty,
+										int32 *buffers_skipped);
+extern void MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
+										int32 *buffers_already_dirty,
+										int32 *buffers_skipped);
 
 /* 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 67431208e7f..3510218a495 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6762,6 +6762,194 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 	}
 }
 
+/*
+ * Helper function to mark unpinned buffer dirty whose buffer header lock is
+ * already acquired.
+ */
+static bool
+MarkDirtyUnpinnedBufferInternal(Buffer buf, BufferDesc *desc,
+								bool *buffer_already_dirty)
+{
+	uint32		buf_state;
+	bool		result = false;
+
+	*buffer_already_dirty = false;
+
+	buf_state = pg_atomic_read_u32(&(desc->state));
+	Assert(buf_state & BM_LOCKED);
+
+	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;
+	}
+
+	/* Pin the buffer and then release the buffer spinlock */
+	PinBuffer_Locked(desc);
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		result = true;
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+	else
+		*buffer_already_dirty = true;
+
+	UnpinBuffer(desc);
+
+	return result;
+}
+
+/*
+ * 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.
+ *
+ * The buffer_already_dirty parameter is mandatory and indicate if the buffer
+ * could not be dirtied because it is already dirty.
+ *
+ * Returns true if the buffer has successfully been marked as dirty.
+ */
+bool
+MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty)
+{
+	BufferDesc *desc;
+	bool		buffer_dirtied = false;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+	LockBufHdr(desc);
+
+	buffer_dirtied = MarkDirtyUnpinnedBufferInternal(buf, desc, buffer_already_dirty);
+	/* Both can not be true at the same time */
+	Assert(!(buffer_dirtied && *buffer_already_dirty));
+
+	return buffer_dirtied;
+}
+
+/*
+ * Try to mark all the shared buffers containing provided relation's pages as
+ * dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkDirtyUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_dirtied - were dirtied
+ * - buffers_already_dirty - were already dirty
+ * - buffers_skipped - could not be dirtied because of the reasons different
+ * than buffer being already dirty
+ */
+void
+MarkDirtyRelUnpinnedBuffers(Relation rel,
+							int32 *buffers_dirtied,
+							int32 *buffers_already_dirty,
+							int32 *buffers_skipped)
+{
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	*buffers_dirtied = 0;
+	*buffers_already_dirty = 0;
+	*buffers_skipped = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state = pg_atomic_read_u32(&(desc->state));
+		bool		buffer_already_dirty;
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		buf_state = LockBufHdr(desc);
+
+		/* recheck, could have changed without the lock */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+		{
+			UnlockBufHdr(desc, buf_state);
+			continue;
+		}
+
+		if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty))
+			(*buffers_dirtied)++;
+		else if (buffer_already_dirty)
+			(*buffers_already_dirty)++;
+		else
+			(*buffers_skipped)++;
+	}
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkDirtyUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_dirtied - were dirtied
+ * - buffers_already_dirty - were already dirty
+ * - buffers_skipped - could not be dirtied because of the reasons different
+ * than buffer being already dirty
+ */
+void
+MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
+							int32 *buffers_already_dirty,
+							int32 *buffers_skipped)
+{
+	*buffers_dirtied = 0;
+	*buffers_already_dirty = 0;
+	*buffers_skipped = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state;
+		bool		buffer_already_dirty;
+
+		buf_state = pg_atomic_read_u32(&desc->state);
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty))
+			(*buffers_dirtied)++;
+		else if (buffer_already_dirty)
+			(*buffers_already_dirty)++;
+		else
+			(*buffers_skipped)++;
+	}
+}
+
 /*
  * 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 eeb85a0e049..719cb561fa4 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -43,6 +43,18 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_relation</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), the
@@ -51,8 +63,11 @@
   <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
   <function>pg_buffercache_evict()</function> function, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function, the
+  <function>pg_buffercache_mark_dirty_relation()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -107,6 +122,25 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty in the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+<para>
+  The <function>pg_buffercache_mark_dirty_relation()</function> function
+  allows all unpinned shared buffers in the relation to be marked as dirty in
+  the buffer pool given a relation identifier.  Use of this function is
+  restricted to superusers only.
+</para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function allows all
+  unpinned shared buffers to be marked as 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>
 
@@ -530,6 +564,59 @@
   </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
+   information about whether the buffer was marked as dirty.  The
+   buffer_dirtied column is true on success, and false if the buffer was
+   already dirty, if the buffer wasn't valid, if it couldn't be marked as
+   dirty because it was pinned.  The buffer_already_dirty column is true if
+   the buffer couldn't be marked as dirty because it was already dirty.  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-relation">
+  <title>The <structname>pg_buffercache_mark_dirty_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_relation()</function> function is
+   very similar to the
+   <function>pg_buffercache_mark_dirty_relation()</function> function. The
+   difference is, the
+   <function>pg_buffercache_mark_dirty_relation()</function> function takes a
+   relation identifier instead of buffer identifier.  It tries to mark all
+   buffers dirty for all forks in that relation.
+
+   It returns the number of marked as dirty buffers, already dirty buffers and
+   the skipped buffers for reasons other than buffers being already dirty.
+   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.
+
+   It returns the number of marked as dirty buffers, already dirty buffers and
+   the skipped buffers for reasons other than buffers being already dirty.
+   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..6b57dff2b61 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,12 @@ 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_relation(1);
+ERROR:  must be superuser to use pg_buffercache_mark_dirty_relation()
+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 +88,18 @@ SELECT * FROM pg_buffercache_evict_relation(NULL);
                  |                 |                
 (1 row)
 
+SELECT * FROM pg_buffercache_mark_dirty(NULL);
+ buffer_dirtied | buffer_already_dirty 
+----------------+----------------------
+                | 
+(1 row)
+
+SELECT * FROM pg_buffercache_mark_dirty_relation(NULL);
+ buffers_dirtied | buffers_already_dirty | buffers_skipped 
+-----------------+-----------------------+-----------------
+                 |                       |                
+(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,11 +109,18 @@ SELECT * FROM pg_buffercache_evict(0);
 ERROR:  bad buffer ID: 0
 SELECT * FROM pg_buffercache_evict(:max_buffers);
 ERROR:  bad buffer ID: 2147483647
--- This should fail because pg_buffercache_evict_relation() doesn't accept
--- local relations
+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
+-- These should fail because they don't accept local relations
 CREATE TEMP TABLE temp_pg_buffercache();
 SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
 ERROR:  relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only
+SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache');
+ERROR:  relation uses local buffers, pg_buffercache_mark_dirty_relation() is intended to be used for shared buffers only
 DROP TABLE temp_pg_buffercache;
 -- These shouldn't fail
 SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1);
@@ -117,5 +142,23 @@ SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg
  t
 (1 row)
 
+SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(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..10fce1ba169
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
@@ -0,0 +1,26 @@
+/* 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,
+    OUT buffer_dirtied boolean,
+    OUT buffer_already_dirty boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_relation(
+    IN regclass,
+    OUT buffers_dirtied int4,
+    OUT buffers_already_dirty int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all(
+    OUT buffers_dirtied int4,
+    OUT buffers_already_dirty int4,
+    OUT buffers_skipped 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 ae0291e6e96..2332db3eed5 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -25,6 +25,9 @@
 #define NUM_BUFFERCACHE_EVICT_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3
 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3
+#define NUM_BUFFERCACHE_MARK_DIRTY_ELEM 2
+#define NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM 3
+#define NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM 3
 
 #define NUM_BUFFERCACHE_NUMA_ELEM	3
 
@@ -100,6 +103,9 @@ 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_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -771,3 +777,120 @@ 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)
+{
+
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_ELEM] = {0};
+
+	Buffer		buf = PG_GETARG_INT32(0);
+	bool		buffer_already_dirty;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty");
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+
+	values[0] = BoolGetDatum(MarkDirtyUnpinnedBuffer(buf, &buffer_already_dirty));
+	values[1] = BoolGetDatum(buffer_already_dirty);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to mark specified relation dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+
+	int32		buffers_already_dirty = 0;
+	int32		buffers_dirtied = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_relation");
+
+	relOid = PG_GETARG_OID(0);
+
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only",
+						"pg_buffercache_mark_dirty_relation")));
+
+	MarkDirtyRelUnpinnedBuffers(rel, &buffers_dirtied, &buffers_already_dirty,
+								&buffers_skipped);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_dirtied);
+	values[1] = Int32GetDatum(buffers_already_dirty);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM] = {0};
+
+	int32		buffers_already_dirty = 0;
+	int32		buffers_dirtied = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_all");
+
+	MarkDirtyAllUnpinnedBuffers(&buffers_dirtied, &buffers_already_dirty,
+								&buffers_skipped);
+
+	values[0] = Int32GetDatum(buffers_dirtied);
+	values[1] = Int32GetDatum(buffers_already_dirty);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 47cca1907c7..1aa3caecd6f 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,17 @@ 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_relation(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);
+SELECT * FROM pg_buffercache_mark_dirty_relation(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,11 +58,14 @@ 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
+-- These should fail because they don't accept local relations
 CREATE TEMP TABLE temp_pg_buffercache();
 SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache');
 DROP TABLE temp_pg_buffercache;
 
 -- These shouldn't fail
@@ -65,6 +73,9 @@ SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1);
 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');
+SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_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.50.1

#7Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Xuneng Zhou (#5)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

Thank you for looking into this! And sorry for the late reply.

On Fri, 16 May 2025 at 10:58, Xuneng Zhou <xunengzhou@gmail.com> wrote:

Hey,

I noticed a couple of small clarity issues in the current version of patch for potential clean up:

1. Commit message wording

I changed the commit message. I made it very similar to the commit
message in dcf7e1697b.

We currently have:

PinBuffer_Locked(desc); /* releases spinlock */

Folks who’re unfamiliar with this function might get confused. Maybe we could use the one in GetVictimBuffer:

/* Pin the buffer and then release its spinlock */

PinBuffer_Locked(buf_hdr);

That spelling-out makes it obvious what’s happening.

I think this makes sense, this is done in v8 which is attached to the
email above.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#8Michael Paquier
michael@paquier.xyz
In reply to: Nazir Bilal Yavuz (#6)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

On Fri, Aug 08, 2025 at 01:16:57PM +0300, Nazir Bilal Yavuz wrote:

Thank you for looking into this! And sorry for the late reply.

Could you rebase, please? This has not applied for some time, but
I've made the situation worse with 4b203d499c61. No need to bump
again the module for this release cycle, we can stay at 1.7 for the
new objects.
--
Michael

#9邱宇航
iamqyh@gmail.com
In reply to: Nazir Bilal Yavuz (#1)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

I suggest using a conditional lock on the buffer, which would be more
appropriate here. Additionally, the function should return whether
the buffer is marked as dirty, the number of buffers marked as dirty.

This change would also make pg_buffercache_mark_dirty_{relation, all}
behave more consistently with pg_buffercache_evict_{relation,all}.

Lastly, `CHECK_FOR_INTERRUPTS()` should be added inside the loop over
`NBuffers` to ensure it can be interrupted during long-running
operations.

Best regards,
Yuhang Qiu

#10Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Michael Paquier (#8)
1 attachment(s)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

On Mon, 24 Nov 2025 at 08:46, Michael Paquier <michael@paquier.xyz> wrote:

On Fri, Aug 08, 2025 at 01:16:57PM +0300, Nazir Bilal Yavuz wrote:

Thank you for looking into this! And sorry for the late reply.

Could you rebase, please? This has not applied for some time, but
I've made the situation worse with 4b203d499c61. No need to bump
again the module for this release cycle, we can stay at 1.7 for the
new objects.
--
Michael

Thanks for the heads up! It is rebased, I also added
'CHECK_FOR_INTERRUPTS()' while looping over the 'NBuffers' regarding
the comment in the email [1]/messages/by-id/D5BB1D85-0F2A-419F-A7B1-426505525D3A@gmail.com.

[1]: /messages/by-id/D5BB1D85-0F2A-419F-A7B1-426505525D3A@gmail.com

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v9-0001-Add-pg_buffercache_mark_dirty-_relation-_all-func.patchtext/x-patch; charset=US-ASCII; name=v9-0001-Add-pg_buffercache_mark_dirty-_relation-_all-func.patchDownload
From 8e1d4584f67f1d5b80c76d86d1c196ceb9f67618 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:39:49 +0300
Subject: [PATCH v9] Add pg_buffercache_mark_dirty{,_relation,_all}() functions

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

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirt_relation(): Marks all shared buffers as dirty
in a relation at once.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty at
once.

The pg_buffercache_mark_dirty_relation() and
pg_buffercache_mark_dirty_all() functions provide mechanism to mark
multiple shared buffers as dirty at once. They are designed to address
the inefficiency of repeatedly calling
pg_buffercache_mark_dirty() for each individual buffer, which can be
time-consuming when dealing with with large shared buffers pool. (e.g.,
~550ms vs. ~70ms for 16GB of fully populated shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Minimal tests for the new functions are included.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Xuneng Zhou <xunengzhou@gmail.com>
Reviewed-by: Yuhang Qiu <iamqyh@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |   8 +
 src/backend/storage/buffer/bufmgr.c           | 192 ++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               |  91 ++++++++-
 .../expected/pg_buffercache.out               |  49 ++++-
 .../pg_buffercache--1.6--1.7.sql              |  23 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 123 +++++++++++
 contrib/pg_buffercache/sql/pg_buffercache.sql |  17 +-
 7 files changed, 495 insertions(+), 8 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index b5f8f3c5d42..9f6785910e0 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -323,6 +323,14 @@ extern void EvictRelUnpinnedBuffers(Relation rel,
 									int32 *buffers_evicted,
 									int32 *buffers_flushed,
 									int32 *buffers_skipped);
+extern bool MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty);
+extern void MarkDirtyRelUnpinnedBuffers(Relation rel,
+										int32 *buffers_dirtied,
+										int32 *buffers_already_dirty,
+										int32 *buffers_skipped);
+extern void MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
+										int32 *buffers_already_dirty,
+										int32 *buffers_skipped);
 
 /* 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 327ddb7adc8..cd34c8146df 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6776,6 +6776,198 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 	}
 }
 
+/*
+ * Helper function to mark unpinned buffer dirty whose buffer header lock is
+ * already acquired.
+ */
+static bool
+MarkDirtyUnpinnedBufferInternal(Buffer buf, BufferDesc *desc,
+								bool *buffer_already_dirty)
+{
+	uint32		buf_state;
+	bool		result = false;
+
+	*buffer_already_dirty = false;
+
+	buf_state = pg_atomic_read_u32(&(desc->state));
+	Assert(buf_state & BM_LOCKED);
+
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc);
+		return false;
+	}
+
+	/* Pin the buffer and then release the buffer spinlock */
+	PinBuffer_Locked(desc);
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		result = true;
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+	else
+		*buffer_already_dirty = true;
+
+	UnpinBuffer(desc);
+
+	return result;
+}
+
+/*
+ * 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.
+ *
+ * The buffer_already_dirty parameter is mandatory and indicate if the buffer
+ * could not be dirtied because it is already dirty.
+ *
+ * Returns true if the buffer has successfully been marked as dirty.
+ */
+bool
+MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty)
+{
+	BufferDesc *desc;
+	bool		buffer_dirtied = false;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+	LockBufHdr(desc);
+
+	buffer_dirtied = MarkDirtyUnpinnedBufferInternal(buf, desc, buffer_already_dirty);
+	/* Both can not be true at the same time */
+	Assert(!(buffer_dirtied && *buffer_already_dirty));
+
+	return buffer_dirtied;
+}
+
+/*
+ * Try to mark all the shared buffers containing provided relation's pages as
+ * dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkDirtyUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_dirtied - were dirtied
+ * - buffers_already_dirty - were already dirty
+ * - buffers_skipped - could not be dirtied because of the reasons different
+ * than buffer being already dirty
+ */
+void
+MarkDirtyRelUnpinnedBuffers(Relation rel,
+							int32 *buffers_dirtied,
+							int32 *buffers_already_dirty,
+							int32 *buffers_skipped)
+{
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	*buffers_dirtied = 0;
+	*buffers_already_dirty = 0;
+	*buffers_skipped = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state = pg_atomic_read_u32(&(desc->state));
+		bool		buffer_already_dirty;
+
+		CHECK_FOR_INTERRUPTS();
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		buf_state = LockBufHdr(desc);
+
+		/* recheck, could have changed without the lock */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+		{
+			UnlockBufHdr(desc);
+			continue;
+		}
+
+		if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty))
+			(*buffers_dirtied)++;
+		else if (buffer_already_dirty)
+			(*buffers_already_dirty)++;
+		else
+			(*buffers_skipped)++;
+	}
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkDirtyUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_dirtied - were dirtied
+ * - buffers_already_dirty - were already dirty
+ * - buffers_skipped - could not be dirtied because of the reasons different
+ * than buffer being already dirty
+ */
+void
+MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
+							int32 *buffers_already_dirty,
+							int32 *buffers_skipped)
+{
+	*buffers_dirtied = 0;
+	*buffers_already_dirty = 0;
+	*buffers_skipped = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state;
+		bool		buffer_already_dirty;
+
+		CHECK_FOR_INTERRUPTS();
+
+		buf_state = pg_atomic_read_u32(&desc->state);
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty))
+			(*buffers_dirtied)++;
+		else if (buffer_already_dirty)
+			(*buffers_already_dirty)++;
+		else
+			(*buffers_skipped)++;
+	}
+}
+
 /*
  * 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 91bbedff343..f3860209c3b 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -43,6 +43,18 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_relation</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), the
@@ -52,8 +64,11 @@
   <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
   <function>pg_buffercache_evict()</function> function, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function, the
+  <function>pg_buffercache_mark_dirty_relation()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -112,6 +127,25 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty in the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+<para>
+  The <function>pg_buffercache_mark_dirty_relation()</function> function
+  allows all unpinned shared buffers in the relation to be marked as dirty in
+  the buffer pool given a relation identifier.  Use of this function is
+  restricted to superusers only.
+</para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function allows all
+  unpinned shared buffers to be marked as 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>
 
@@ -582,6 +616,59 @@
   </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
+   information about whether the buffer was marked as dirty.  The
+   buffer_dirtied column is true on success, and false if the buffer was
+   already dirty, if the buffer wasn't valid, if it couldn't be marked as
+   dirty because it was pinned.  The buffer_already_dirty column is true if
+   the buffer couldn't be marked as dirty because it was already dirty.  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-relation">
+  <title>The <structname>pg_buffercache_mark_dirty_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_relation()</function> function is
+   very similar to the
+   <function>pg_buffercache_mark_dirty_relation()</function> function. The
+   difference is, the
+   <function>pg_buffercache_mark_dirty_relation()</function> function takes a
+   relation identifier instead of buffer identifier.  It tries to mark all
+   buffers dirty for all forks in that relation.
+
+   It returns the number of marked as dirty buffers, already dirty buffers and
+   the skipped buffers for reasons other than buffers being already dirty.
+   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.
+
+   It returns the number of marked as dirty buffers, already dirty buffers and
+   the skipped buffers for reasons other than buffers being already dirty.
+   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/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index 26c2d5f5710..886dea770f6 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -75,7 +75,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;
@@ -86,6 +86,12 @@ 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_relation(1);
+ERROR:  must be superuser to use pg_buffercache_mark_dirty_relation()
+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);
@@ -100,6 +106,18 @@ SELECT * FROM pg_buffercache_evict_relation(NULL);
                  |                 |                
 (1 row)
 
+SELECT * FROM pg_buffercache_mark_dirty(NULL);
+ buffer_dirtied | buffer_already_dirty 
+----------------+----------------------
+                | 
+(1 row)
+
+SELECT * FROM pg_buffercache_mark_dirty_relation(NULL);
+ buffers_dirtied | buffers_already_dirty | buffers_skipped 
+-----------------+-----------------------+-----------------
+                 |                       |                
+(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
@@ -109,11 +127,18 @@ SELECT * FROM pg_buffercache_evict(0);
 ERROR:  bad buffer ID: 0
 SELECT * FROM pg_buffercache_evict(:max_buffers);
 ERROR:  bad buffer ID: 2147483647
--- This should fail because pg_buffercache_evict_relation() doesn't accept
--- local relations
+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
+-- These should fail because they don't accept local relations
 CREATE TEMP TABLE temp_pg_buffercache();
 SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
 ERROR:  relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only
+SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache');
+ERROR:  relation uses local buffers, pg_buffercache_mark_dirty_relation() is intended to be used for shared buffers only
 DROP TABLE temp_pg_buffercache;
 -- These shouldn't fail
 SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1);
@@ -135,5 +160,23 @@ SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg
  t
 (1 row)
 
+SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(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/pg_buffercache--1.6--1.7.sql b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
index 5ecc0a8708a..b39f96f5186 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
@@ -31,3 +31,26 @@ REVOKE ALL ON pg_buffercache_numa FROM PUBLIC;
 GRANT EXECUTE ON FUNCTION pg_buffercache_os_pages(boolean) TO pg_monitor;
 GRANT SELECT ON pg_buffercache_os_pages TO pg_monitor;
 GRANT SELECT ON pg_buffercache_numa TO pg_monitor;
+
+
+CREATE FUNCTION pg_buffercache_mark_dirty(
+    IN int,
+    OUT buffer_dirtied boolean,
+    OUT buffer_already_dirty boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_relation(
+    IN regclass,
+    OUT buffers_dirtied int4,
+    OUT buffers_already_dirty int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all(
+    OUT buffers_dirtied int4,
+    OUT buffers_already_dirty int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index ae1712fc93c..6472f511530 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -25,6 +25,9 @@
 #define NUM_BUFFERCACHE_EVICT_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3
 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3
+#define NUM_BUFFERCACHE_MARK_DIRTY_ELEM 2
+#define NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM 3
+#define NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM 3
 
 #define NUM_BUFFERCACHE_OS_PAGES_ELEM	3
 
@@ -101,6 +104,9 @@ 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_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -826,3 +832,120 @@ 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)
+{
+
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_ELEM] = {0};
+
+	Buffer		buf = PG_GETARG_INT32(0);
+	bool		buffer_already_dirty;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty");
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+
+	values[0] = BoolGetDatum(MarkDirtyUnpinnedBuffer(buf, &buffer_already_dirty));
+	values[1] = BoolGetDatum(buffer_already_dirty);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to mark specified relation dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+
+	int32		buffers_already_dirty = 0;
+	int32		buffers_dirtied = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_relation");
+
+	relOid = PG_GETARG_OID(0);
+
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only",
+						"pg_buffercache_mark_dirty_relation")));
+
+	MarkDirtyRelUnpinnedBuffers(rel, &buffers_dirtied, &buffers_already_dirty,
+								&buffers_skipped);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_dirtied);
+	values[1] = Int32GetDatum(buffers_already_dirty);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM] = {0};
+
+	int32		buffers_already_dirty = 0;
+	int32		buffers_dirtied = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_all");
+
+	MarkDirtyAllUnpinnedBuffers(&buffers_dirtied, &buffers_already_dirty,
+								&buffers_skipped);
+
+	values[0] = Int32GetDatum(buffers_dirtied);
+	values[1] = Int32GetDatum(buffers_already_dirty);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 3c70ee9ef4a..127d604905c 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -38,7 +38,7 @@ RESET role;
 
 
 ------
----- Test pg_buffercache_evict* functions
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
 ------
 
 CREATE ROLE regress_buffercache_normal;
@@ -48,12 +48,17 @@ 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_relation(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);
+SELECT * FROM pg_buffercache_mark_dirty_relation(NULL);
 
 -- These should fail because they are not called by valid range of buffers
 -- Number of the shared buffers are limited by max integer
@@ -61,11 +66,14 @@ 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
+-- These should fail because they don't accept local relations
 CREATE TEMP TABLE temp_pg_buffercache();
 SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache');
 DROP TABLE temp_pg_buffercache;
 
 -- These shouldn't fail
@@ -73,6 +81,9 @@ SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1);
 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');
+SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_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.51.0

#11Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: 邱宇航 (#9)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

Thank you for looking into this!

On Mon, 24 Nov 2025 at 09:51, 邱宇航 <iamqyh@gmail.com> wrote:

I suggest using a conditional lock on the buffer, which would be more
appropriate here.

Could you please explain that a bit more? AFAIU, conditional locks are
mainly used to escape from deadlock situations and we can not cause a
deadlock here. Is it because using conditional locks might make the
functions faster by skipping the wait situations?

Additionally, the function should return whether
the buffer is marked as dirty, the number of buffers marked as dirty.

This change would also make pg_buffercache_mark_dirty_{relation, all}
behave more consistently with pg_buffercache_evict_{relation,all}.

Do you mean that we should not return 'buffer_already_dirty'
information and we should only return 'buffer_dirtied' information in
the 'pg_buffercache_mark_dirty' function? If you are suggesting that,
I think returning 'buffer_already_dirty' has a value in it.

Lastly, `CHECK_FOR_INTERRUPTS()` should be added inside the loop over
`NBuffers` to ensure it can be interrupted during long-running
operations.

You are right, applied to v9 in the email below [1]/messages/by-id/CAN55FZ1E4ruwjjarUc0WoHxSpW=CZ0aEPfSrUC4z65UtEM7DNw@mail.gmail.com.

[1]: /messages/by-id/CAN55FZ1E4ruwjjarUc0WoHxSpW=CZ0aEPfSrUC4z65UtEM7DNw@mail.gmail.com

--
Regards,
Nazir Bilal Yavuz
Microsoft

#12邱宇航
iamqyh@gmail.com
In reply to: Nazir Bilal Yavuz (#11)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

2025年11月24日 15:50,Nazir Bilal Yavuz <byavuz81@gmail.com> 写道:

Could you please explain that a bit more? AFAIU, conditional locks are
mainly used to escape from deadlock situations and we can not cause a
deadlock here. Is it because using conditional locks might make the
functions faster by skipping the wait situations?

Bgwriter/Checkpointer might always blocks the mark buffer dirty SQL.

sequence Bgwriter/Checkpointer mark-buffer-dirty SQL
1 LockBuffer(1) WaitBuffer(1)
2 UnlockBuffer(1),
and LockBuffer(2)
3 After unlock wakeup,
WaitBuffer(2)
... ... ...

I don't know if this could really happen. Maybe we need some tests. I
just afraid that pg_buffercache_mark_dirty_{relation, all} SQL could be
slow and inefficient.

Do you mean that we should not return 'buffer_already_dirty'
information and we should only return 'buffer_dirtied' information in
the 'pg_buffercache_mark_dirty' function? If you are suggesting that,
I think returning 'buffer_already_dirty' has a value in it.

Oops, I read the v7 patch. Ignore this.

Best regards,
Yuhang Qiu

#13Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: 邱宇航 (#12)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

On Mon, 24 Nov 2025 at 11:47, 邱宇航 <iamqyh@gmail.com> wrote:

2025年11月24日 15:50,Nazir Bilal Yavuz <byavuz81@gmail.com> 写道:

Could you please explain that a bit more? AFAIU, conditional locks are
mainly used to escape from deadlock situations and we can not cause a
deadlock here. Is it because using conditional locks might make the
functions faster by skipping the wait situations?

Bgwriter/Checkpointer might always blocks the mark buffer dirty SQL.

sequence Bgwriter/Checkpointer mark-buffer-dirty SQL
1 LockBuffer(1) WaitBuffer(1)
2 UnlockBuffer(1),
and LockBuffer(2)
3 After unlock wakeup,
WaitBuffer(2)
... ... ...

I don't know if this could really happen. Maybe we need some tests. I
just afraid that pg_buffercache_mark_dirty_{relation, all} SQL could be
slow and inefficient.

I do not think that will be a problem but I can change it if the
general consensus is towards this way. Also, if we change this for
pg_buffercache_mark_dirty_* functions, I think we need to apply the
same for the pg_buffercache_evict_* functions.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#14Michael Paquier
michael@paquier.xyz
In reply to: Nazir Bilal Yavuz (#10)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

On Mon, Nov 24, 2025 at 10:48:24AM +0300, Nazir Bilal Yavuz wrote:

Thanks for the heads up! It is rebased, I also added
'CHECK_FOR_INTERRUPTS()' while looping over the 'NBuffers' regarding
the comment in the email [1].

[1] /messages/by-id/D5BB1D85-0F2A-419F-A7B1-426505525D3A@gmail.com

The approach looks acceptable here. MarkDirtyUnpinnedBufferInternal()
is used as a workhorse to limit the duplication in the three routines
you are adding, and it is a slightly disappointing that the loops for
the buffers are duplicated, particularly for the relation vs the all
case, but we've also lived with these in core for the buffer eviction
and pg_buffercache. One thing that would make more sense to me is to
split the patch in two: one for the changes in bufmgr.c/h and one for
pg_buffercache. There can be an argument that these new APIs could be
useful for out-of-core code, as well, to let developers play with the
state of the buffers. I mean, why not, the thread is also about API
extensibility, as well, to enforce dirty states. :)

Any thoughts or comments from others?
--
Michael

#15邱宇航
iamqyh@gmail.com
In reply to: Nazir Bilal Yavuz (#13)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

I do not think that will be a problem but I can change it if the
general consensus is towards this way. Also, if we change this for
pg_buffercache_mark_dirty_* functions, I think we need to apply the
same for the pg_buffercache_evict_* functions.

After some testing, bgwriter/checkpointer didn' blocks the mark buffer
dirty SQL. it's ok to use LWLockAcquire.

There is an extra line break after elog(ERROR, "bad buffer ID: %d", buf)
which can be removed.

Best regards,
Yuhang Qiu

#16邱宇航
iamqyh@gmail.com
In reply to: Michael Paquier (#14)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

It is a slightly disappointing that the loops for
the buffers are duplicated, particularly for the relation vs the all
case.

Yes, and we got another two loops in pg_buffercache_evict functions,
and more loops in Drop/Flush relation/database buffers functions. Maybe
we can abstract them into a generic loop function and it takes a buffer
handler function pointer to avoid duplication?

One thing that would make more sense to me is to
split the patch in two: one for the changes in bufmgr.c/h and one for
pg_buffercache. There can be an argument that these new APIs could be
useful for out-of-core code, as well, to let developers play with the
state of the buffers. I mean, why not, the thread is also about API
extensibility, as well, to enforce dirty states. :)

Agreed. It might be three, one for generic loop function, one for API
and one for pg_buffercache. If we put new API out-of-core, the latter
two can be merged.

Best regards,
Yuhang Qiu

#17Michael Paquier
michael@paquier.xyz
In reply to: 邱宇航 (#16)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

On Thu, Nov 27, 2025 at 11:07:43AM +0800, 邱宇航 wrote:

Yes, and we got another two loops in pg_buffercache_evict functions,
and more loops in Drop/Flush relation/database buffers functions. Maybe
we can abstract them into a generic loop function and it takes a buffer
handler function pointer to avoid duplication?

I was considering an option when looking at the patch this morning,
but could not get behind it as it hides the internals of the routines
inside one extra layer of routines.. So what Nazir has done seems
like a balance good enough, at least for me.
--
Michael

#18Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Michael Paquier (#14)
2 attachment(s)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

On Thu, 27 Nov 2025 at 02:35, Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Nov 24, 2025 at 10:48:24AM +0300, Nazir Bilal Yavuz wrote:

Thanks for the heads up! It is rebased, I also added
'CHECK_FOR_INTERRUPTS()' while looping over the 'NBuffers' regarding
the comment in the email [1].

[1] /messages/by-id/D5BB1D85-0F2A-419F-A7B1-426505525D3A@gmail.com

One thing that would make more sense to me is to
split the patch in two: one for the changes in bufmgr.c/h and one for
pg_buffercache. There can be an argument that these new APIs could be
useful for out-of-core code, as well, to let developers play with the
state of the buffers. I mean, why not, the thread is also about API
extensibility, as well, to enforce dirty states. :)

Any thoughts or comments from others?

I agree with you, the patches make more sense this way.

The patches are split into two in v10. There are no changes from v9,
except that one extra blank line was removed [1]/messages/by-id/188562F6-5BBB-49AB-B9E1-6312AE7970E8@gmail.com.

[1]: /messages/by-id/188562F6-5BBB-49AB-B9E1-6312AE7970E8@gmail.com

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v10-0001-Add-internal-APIs-for-marking-buffers-dirty-effi.patchtext/x-patch; charset=US-ASCII; name=v10-0001-Add-internal-APIs-for-marking-buffers-dirty-effi.patchDownload
From c5f8bac945a622d4c8965f202b4dddc84438595c Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Thu, 27 Nov 2025 10:08:27 +0300
Subject: [PATCH v10 1/2] Add internal APIs for marking buffers dirty
 efficiently

This commit introduces new internal bufmgr APIs for marking shared
buffers as dirty:

- MarkDirtyUnpinnedBufferInternal()
- MarkDirtyUnpinnedBuffer()
- MarkDirtyRelUnpinnedBuffers()
- MarkDirtyAllUnpinnedBuffers()

These functions provide efficient mechanisms to mark one buffer, all
buffers of a relation, or the entire shared buffer pool as dirty.

These APIs are intended primarily for developer tooling, debugging, and
extensions that need to manipulate buffer dirtiness in bulk. They are
not exposed through SQL in this commit and there are no user-visible
changes. They will be used in the pg_buffercache extension in the
subsequent commit.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Xuneng Zhou <xunengzhou@gmail.com>
Reviewed-by: Yuhang Qiu <iamqyh@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h        |   8 ++
 src/backend/storage/buffer/bufmgr.c | 192 ++++++++++++++++++++++++++++
 2 files changed, 200 insertions(+)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index b5f8f3c5d42..9f6785910e0 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -323,6 +323,14 @@ extern void EvictRelUnpinnedBuffers(Relation rel,
 									int32 *buffers_evicted,
 									int32 *buffers_flushed,
 									int32 *buffers_skipped);
+extern bool MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty);
+extern void MarkDirtyRelUnpinnedBuffers(Relation rel,
+										int32 *buffers_dirtied,
+										int32 *buffers_already_dirty,
+										int32 *buffers_skipped);
+extern void MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
+										int32 *buffers_already_dirty,
+										int32 *buffers_skipped);
 
 /* 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 327ddb7adc8..cd34c8146df 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6776,6 +6776,198 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 	}
 }
 
+/*
+ * Helper function to mark unpinned buffer dirty whose buffer header lock is
+ * already acquired.
+ */
+static bool
+MarkDirtyUnpinnedBufferInternal(Buffer buf, BufferDesc *desc,
+								bool *buffer_already_dirty)
+{
+	uint32		buf_state;
+	bool		result = false;
+
+	*buffer_already_dirty = false;
+
+	buf_state = pg_atomic_read_u32(&(desc->state));
+	Assert(buf_state & BM_LOCKED);
+
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc);
+		return false;
+	}
+
+	/* Pin the buffer and then release the buffer spinlock */
+	PinBuffer_Locked(desc);
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		result = true;
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+	else
+		*buffer_already_dirty = true;
+
+	UnpinBuffer(desc);
+
+	return result;
+}
+
+/*
+ * 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.
+ *
+ * The buffer_already_dirty parameter is mandatory and indicate if the buffer
+ * could not be dirtied because it is already dirty.
+ *
+ * Returns true if the buffer has successfully been marked as dirty.
+ */
+bool
+MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty)
+{
+	BufferDesc *desc;
+	bool		buffer_dirtied = false;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+	LockBufHdr(desc);
+
+	buffer_dirtied = MarkDirtyUnpinnedBufferInternal(buf, desc, buffer_already_dirty);
+	/* Both can not be true at the same time */
+	Assert(!(buffer_dirtied && *buffer_already_dirty));
+
+	return buffer_dirtied;
+}
+
+/*
+ * Try to mark all the shared buffers containing provided relation's pages as
+ * dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkDirtyUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_dirtied - were dirtied
+ * - buffers_already_dirty - were already dirty
+ * - buffers_skipped - could not be dirtied because of the reasons different
+ * than buffer being already dirty
+ */
+void
+MarkDirtyRelUnpinnedBuffers(Relation rel,
+							int32 *buffers_dirtied,
+							int32 *buffers_already_dirty,
+							int32 *buffers_skipped)
+{
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	*buffers_dirtied = 0;
+	*buffers_already_dirty = 0;
+	*buffers_skipped = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state = pg_atomic_read_u32(&(desc->state));
+		bool		buffer_already_dirty;
+
+		CHECK_FOR_INTERRUPTS();
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		buf_state = LockBufHdr(desc);
+
+		/* recheck, could have changed without the lock */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+		{
+			UnlockBufHdr(desc);
+			continue;
+		}
+
+		if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty))
+			(*buffers_dirtied)++;
+		else if (buffer_already_dirty)
+			(*buffers_already_dirty)++;
+		else
+			(*buffers_skipped)++;
+	}
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ *
+ * This function is intended for testing/development use only! See
+ * MarkDirtyUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_dirtied - were dirtied
+ * - buffers_already_dirty - were already dirty
+ * - buffers_skipped - could not be dirtied because of the reasons different
+ * than buffer being already dirty
+ */
+void
+MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
+							int32 *buffers_already_dirty,
+							int32 *buffers_skipped)
+{
+	*buffers_dirtied = 0;
+	*buffers_already_dirty = 0;
+	*buffers_skipped = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state;
+		bool		buffer_already_dirty;
+
+		CHECK_FOR_INTERRUPTS();
+
+		buf_state = pg_atomic_read_u32(&desc->state);
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty))
+			(*buffers_dirtied)++;
+		else if (buffer_already_dirty)
+			(*buffers_already_dirty)++;
+		else
+			(*buffers_skipped)++;
+	}
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
-- 
2.51.0

v10-0002-Add-pg_buffercache_mark_dirty-_relation-_all-fun.patchtext/x-patch; charset=US-ASCII; name=v10-0002-Add-pg_buffercache_mark_dirty-_relation-_all-fun.patchDownload
From f8b305e272255f2f1ec1d1434316a6466aa18232 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Thu, 27 Nov 2025 10:13:44 +0300
Subject: [PATCH v10 2/2] Add pg_buffercache_mark_dirty{,_relation,_all}()
 functions

This commit introduces three new functions for marking shared buffers as
dirty by using the functions from prior commit:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirt_relation(): Marks all shared buffers as dirty
in a relation at once.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty at
once.

The pg_buffercache_mark_dirty_relation() and
pg_buffercache_mark_dirty_all() functions provide mechanism to mark
multiple shared buffers as dirty at once. They are designed to address
the inefficiency of repeatedly calling
pg_buffercache_mark_dirty() for each individual buffer, which can be
time-consuming when dealing with with large shared buffers pool. (e.g.,
~550ms vs. ~70ms for 16GB of fully populated shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Minimal tests for the new functions are included.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Xuneng Zhou <xunengzhou@gmail.com>
Reviewed-by: Yuhang Qiu <iamqyh@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 doc/src/sgml/pgbuffercache.sgml               |  91 ++++++++++++-
 .../expected/pg_buffercache.out               |  49 ++++++-
 .../pg_buffercache--1.6--1.7.sql              |  23 ++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 122 ++++++++++++++++++
 contrib/pg_buffercache/sql/pg_buffercache.sql |  17 ++-
 5 files changed, 294 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 91bbedff343..f3860209c3b 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -43,6 +43,18 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_relation</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), the
@@ -52,8 +64,11 @@
   <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
   <function>pg_buffercache_evict()</function> function, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function, the
+  <function>pg_buffercache_mark_dirty_relation()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -112,6 +127,25 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty in the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+<para>
+  The <function>pg_buffercache_mark_dirty_relation()</function> function
+  allows all unpinned shared buffers in the relation to be marked as dirty in
+  the buffer pool given a relation identifier.  Use of this function is
+  restricted to superusers only.
+</para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function allows all
+  unpinned shared buffers to be marked as 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>
 
@@ -582,6 +616,59 @@
   </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
+   information about whether the buffer was marked as dirty.  The
+   buffer_dirtied column is true on success, and false if the buffer was
+   already dirty, if the buffer wasn't valid, if it couldn't be marked as
+   dirty because it was pinned.  The buffer_already_dirty column is true if
+   the buffer couldn't be marked as dirty because it was already dirty.  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-relation">
+  <title>The <structname>pg_buffercache_mark_dirty_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_relation()</function> function is
+   very similar to the
+   <function>pg_buffercache_mark_dirty_relation()</function> function. The
+   difference is, the
+   <function>pg_buffercache_mark_dirty_relation()</function> function takes a
+   relation identifier instead of buffer identifier.  It tries to mark all
+   buffers dirty for all forks in that relation.
+
+   It returns the number of marked as dirty buffers, already dirty buffers and
+   the skipped buffers for reasons other than buffers being already dirty.
+   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.
+
+   It returns the number of marked as dirty buffers, already dirty buffers and
+   the skipped buffers for reasons other than buffers being already dirty.
+   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/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index 26c2d5f5710..886dea770f6 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -75,7 +75,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;
@@ -86,6 +86,12 @@ 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_relation(1);
+ERROR:  must be superuser to use pg_buffercache_mark_dirty_relation()
+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);
@@ -100,6 +106,18 @@ SELECT * FROM pg_buffercache_evict_relation(NULL);
                  |                 |                
 (1 row)
 
+SELECT * FROM pg_buffercache_mark_dirty(NULL);
+ buffer_dirtied | buffer_already_dirty 
+----------------+----------------------
+                | 
+(1 row)
+
+SELECT * FROM pg_buffercache_mark_dirty_relation(NULL);
+ buffers_dirtied | buffers_already_dirty | buffers_skipped 
+-----------------+-----------------------+-----------------
+                 |                       |                
+(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
@@ -109,11 +127,18 @@ SELECT * FROM pg_buffercache_evict(0);
 ERROR:  bad buffer ID: 0
 SELECT * FROM pg_buffercache_evict(:max_buffers);
 ERROR:  bad buffer ID: 2147483647
--- This should fail because pg_buffercache_evict_relation() doesn't accept
--- local relations
+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
+-- These should fail because they don't accept local relations
 CREATE TEMP TABLE temp_pg_buffercache();
 SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
 ERROR:  relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only
+SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache');
+ERROR:  relation uses local buffers, pg_buffercache_mark_dirty_relation() is intended to be used for shared buffers only
 DROP TABLE temp_pg_buffercache;
 -- These shouldn't fail
 SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1);
@@ -135,5 +160,23 @@ SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg
  t
 (1 row)
 
+SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(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/pg_buffercache--1.6--1.7.sql b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
index 5ecc0a8708a..b39f96f5186 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
@@ -31,3 +31,26 @@ REVOKE ALL ON pg_buffercache_numa FROM PUBLIC;
 GRANT EXECUTE ON FUNCTION pg_buffercache_os_pages(boolean) TO pg_monitor;
 GRANT SELECT ON pg_buffercache_os_pages TO pg_monitor;
 GRANT SELECT ON pg_buffercache_numa TO pg_monitor;
+
+
+CREATE FUNCTION pg_buffercache_mark_dirty(
+    IN int,
+    OUT buffer_dirtied boolean,
+    OUT buffer_already_dirty boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_relation(
+    IN regclass,
+    OUT buffers_dirtied int4,
+    OUT buffers_already_dirty int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all(
+    OUT buffers_dirtied int4,
+    OUT buffers_already_dirty int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index ae1712fc93c..35d463f9f66 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -25,6 +25,9 @@
 #define NUM_BUFFERCACHE_EVICT_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3
 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3
+#define NUM_BUFFERCACHE_MARK_DIRTY_ELEM 2
+#define NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM 3
+#define NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM 3
 
 #define NUM_BUFFERCACHE_OS_PAGES_ELEM	3
 
@@ -101,6 +104,9 @@ 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_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -826,3 +832,119 @@ 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)
+{
+
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_ELEM] = {0};
+
+	Buffer		buf = PG_GETARG_INT32(0);
+	bool		buffer_already_dirty;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty");
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	values[0] = BoolGetDatum(MarkDirtyUnpinnedBuffer(buf, &buffer_already_dirty));
+	values[1] = BoolGetDatum(buffer_already_dirty);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to mark specified relation dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+
+	int32		buffers_already_dirty = 0;
+	int32		buffers_dirtied = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_relation");
+
+	relOid = PG_GETARG_OID(0);
+
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only",
+						"pg_buffercache_mark_dirty_relation")));
+
+	MarkDirtyRelUnpinnedBuffers(rel, &buffers_dirtied, &buffers_already_dirty,
+								&buffers_skipped);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_dirtied);
+	values[1] = Int32GetDatum(buffers_already_dirty);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM] = {0};
+
+	int32		buffers_already_dirty = 0;
+	int32		buffers_dirtied = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_mark_dirty_all");
+
+	MarkDirtyAllUnpinnedBuffers(&buffers_dirtied, &buffers_already_dirty,
+								&buffers_skipped);
+
+	values[0] = Int32GetDatum(buffers_dirtied);
+	values[1] = Int32GetDatum(buffers_already_dirty);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 3c70ee9ef4a..127d604905c 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -38,7 +38,7 @@ RESET role;
 
 
 ------
----- Test pg_buffercache_evict* functions
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
 ------
 
 CREATE ROLE regress_buffercache_normal;
@@ -48,12 +48,17 @@ 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_relation(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);
+SELECT * FROM pg_buffercache_mark_dirty_relation(NULL);
 
 -- These should fail because they are not called by valid range of buffers
 -- Number of the shared buffers are limited by max integer
@@ -61,11 +66,14 @@ 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
+-- These should fail because they don't accept local relations
 CREATE TEMP TABLE temp_pg_buffercache();
 SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache');
 DROP TABLE temp_pg_buffercache;
 
 -- These shouldn't fail
@@ -73,6 +81,9 @@ SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1);
 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');
+SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_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.51.0

#19Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: 邱宇航 (#15)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

On Thu, 27 Nov 2025 at 05:51, 邱宇航 <iamqyh@gmail.com> wrote:

I do not think that will be a problem but I can change it if the
general consensus is towards this way. Also, if we change this for
pg_buffercache_mark_dirty_* functions, I think we need to apply the
same for the pg_buffercache_evict_* functions.

After some testing, bgwriter/checkpointer didn' blocks the mark buffer
dirty SQL. it's ok to use LWLockAcquire.

Thanks for testing this!

There is an extra line break after elog(ERROR, "bad buffer ID: %d", buf)
which can be removed.

Done in the v10. I wonder why pgindent did not catch this.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#20Michael Paquier
michael@paquier.xyz
In reply to: Nazir Bilal Yavuz (#18)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

On Thu, Nov 27, 2025 at 10:59:47AM +0300, Nazir Bilal Yavuz wrote:

I agree with you, the patches make more sense this way.

The patches are split into two in v10. There are no changes from v9,
except that one extra blank line was removed [1].

Note that there were a couple of things incorrect in the docs. I have
done a sweep to improve the wording in the comments and the docs
themselves, then applied the result.

Testing the valid case for the "_all" function flavor could be costly
for installcheck so I am feeling a bit reserved on its cost. We are
doing it for the evict case as well, so I have kept it at the end to
keep the coverage. If it proves to be an issue or if there is a
concern with this part, I would be OK to remove it.
--
Michael

#21Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Michael Paquier (#20)
Re: Add pg_buffercache_mark_dirty[_all] functions to the pg_buffercache

Hi,

On Fri, 28 Nov 2025 at 03:36, Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Nov 27, 2025 at 10:59:47AM +0300, Nazir Bilal Yavuz wrote:

I agree with you, the patches make more sense this way.

The patches are split into two in v10. There are no changes from v9,
except that one extra blank line was removed [1].

Note that there were a couple of things incorrect in the docs. I have
done a sweep to improve the wording in the comments and the docs
themselves, then applied the result.

Thanks for doing this!

Testing the valid case for the "_all" function flavor could be costly
for installcheck so I am feeling a bit reserved on its cost. We are
doing it for the evict case as well, so I have kept it at the end to
keep the coverage. If it proves to be an issue or if there is a
concern with this part, I would be OK to remove it.

You are right, I did not think of this aspect.

--
Regards,
Nazir Bilal Yavuz
Microsoft