Hi,

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

On Mon, 12 May 2025 at 13:24, Amit Kapila <amit.kapil...@gmail.com> wrote:
>
> On Mon, Apr 28, 2025 at 2:43 PM Nazir Bilal Yavuz <byavu...@gmail.com> wrote:
> >
> > Hi,
> >
> > On Fri, 25 Apr 2025 at 19:17, Xuneng Zhou <xunengz...@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
From 5a850eb46467284b580ba9ea612eb6cff112383a Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavu...@gmail.com>
Date: Fri, 4 Apr 2025 13:39:49 +0300
Subject: [PATCH 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 <byavu...@gmail.com>
Reviewed-by: Aidar Imamov <a.ima...@postgrespro.ru>
Reviewed-by: Andres Freund <and...@anarazel.de>
Reviewed-by: Amit Kapila <amit.kapil...@gmail.com>
Reviewed-by: Joseph Koshakow <kosh...@gmail.com>
Reviewed-by: Xuneng Zhou <xunengz...@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

Reply via email to