pg_buffercache: Add pg_buffercache_mark_dirty{,_relation,_all}()
authorMichael Paquier <[email protected]>
Fri, 28 Nov 2025 00:04:04 +0000 (09:04 +0900)
committerMichael Paquier <[email protected]>
Fri, 28 Nov 2025 00:04:04 +0000 (09:04 +0900)
This commit introduces three new functions for marking shared buffers as
dirty by using the functions introduced in 9660906dbd69:
* pg_buffercache_mark_dirty() for one shared buffer.
- pg_buffercache_mark_dirt_relation() for all the shared buffers in a
relation.
* pg_buffercache_mark_dirty_all() for all the shared buffers in pool.

The "_all" and "_relation" flavors 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.

These functions are intended as developer tools and are available only
to superusers.  There is no need to bump the version of pg_buffercache,
4b203d499c61 having done this job in this release cycle.

Author: Nazir Bilal Yavuz <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Aidar Imamov <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Joseph Koshakow <[email protected]>
Reviewed-by: Michael Paquier <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Xuneng Zhou <[email protected]>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw@mail.gmail.com

contrib/pg_buffercache/expected/pg_buffercache.out
contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql
contrib/pg_buffercache/pg_buffercache_pages.c
contrib/pg_buffercache/sql/pg_buffercache.sql
doc/src/sgml/pgbuffercache.sgml

index 26c2d5f57105529abdb055bf6acba73a9d824e0d..886dea770f62613ecada62b9a9d69597ae9b3a30 100644 (file)
@@ -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;
index 5ecc0a8708a0f02124085a16b9deae895bc72360..9a7bf66dab54b5eed065f4bc5f63a790e223a8d5 100644 (file)
@@ -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;
+
+-- Functions to mark buffers as dirty.
+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;
index ae1712fc93ccfff4ab72df2f05218741d5a10582..702307a49e25ecc1029a2ddbe86c2af0a549a466 100644 (file)
@@ -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 all the shared buffers of a relation as 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);
+}
index 3c70ee9ef4ae28c4db6b617ec94e482e3fcefd22..127d604905ca089e02634e0ea4c94b61b30dd3bf 100644 (file)
@@ -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;
index a011c749c8a043345bdbf9aa6a618ae5aafd2be5..2e1b9bbecf4708eb56d891ded84c0c25cea8db73 100644 (file)
   <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
   <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>
   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>
 
   </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 <structfield>buffer_dirtied</structfield> column is true on success,
+   and false if the buffer was already dirty if the buffer was not valid or
+   if it could not be marked as dirty because it was pinned.
+   The <structfield>buffer_already_dirty</structfield> 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()</function> function.
+   The difference is that 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 buffers marked as dirty, the number of buffers
+   already dirty and the number of buffers skipped because already pinned or
+   invalid.
+   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 that the
+   <function>pg_buffercache_mark_dirty_all()</function> tries to mark all
+   buffers dirty in the buffer pool.
+   It returns the number of buffers marked as dirty, the number of buffers
+   already dirty and the number of buffers skipped because already pinned or
+   invalid.
+   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>