From dd4eae37f3610e0afe82a19efae015abc8e9c6a6 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Mon, 7 Apr 2025 19:21:37 -0400
Subject: [PATCH v8] Add pg_buffercache_evict_{relation,all} functions

In addition to the added functions, the pg_buffercache_evict() function now
shows whether the buffer was flushed.

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be time-consuming
when dealing with large shared buffer pools. (e.g., ~477ms vs. ~2576ms 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. Also, there was no test for
pg_buffercache_evict(), test for this added too.

No new extension version is needed, as it was already increased this release
by ba2a3c2302f.

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                  |   9 +-
 src/backend/storage/buffer/bufmgr.c           | 178 +++++++++++++++---
 .../expected/pg_buffercache.out               |  64 +++++++
 .../pg_buffercache--1.5--1.6.sql              |  24 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 127 ++++++++++++-
 contrib/pg_buffercache/sql/pg_buffercache.sql |  42 +++++
 doc/src/sgml/pgbuffercache.sgml               |  72 ++++++-
 src/test/modules/test_aio/test_aio.c          |   6 +-
 8 files changed, 481 insertions(+), 41 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index f2192ceb271..235b6f18962 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -304,7 +304,14 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
+extern void EvictAllUnpinnedBuffers(int32 *buffers_evicted,
+									int32 *buffers_flushed,
+									int32 *buffers_skipped);
+extern void EvictRelUnpinnedBuffers(Relation rel,
+									int32 *buffers_evicted,
+									int32 *buffers_flushed,
+									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 941d7fa3d94..2c29af36159 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6509,6 +6509,53 @@ ResOwnerPrintBufferPin(Datum res)
 	return DebugPrintBufferRefcount(DatumGetInt32(res));
 }
 
+/*
+ * Helper function to evict unpinned buffer whose buffer header lock is
+ * already acquired.
+ */
+static bool
+EvictUnpinnedBufferInternal(BufferDesc *desc, bool *flushed)
+{
+	uint32		buf_state;
+	bool		result;
+
+	*flushed = 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;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was dirty, try to clean it once. */
+	if (buf_state & BM_DIRTY)
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
+		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
+		*flushed = true;
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+
+	/* This will return false if it becomes dirty or someone else pins it. */
+	result = InvalidateVictimBuffer(desc);
+
+	UnpinBuffer(desc);
+
+	return result;
+}
+
 /*
  * Try to evict the current block in a shared buffer.
  *
@@ -6521,55 +6568,134 @@ ResOwnerPrintBufferPin(Datum res)
  * even by the same block.  This inherent raciness without other interlocking
  * makes the function unsuitable for non-testing usage.
  *
+ * *flushed is set to true if the buffer was dirty and has been flushed, false
+ * otherwise.  However, *flushed=true does not necessarily mean that we
+ * flushed the buffer, it could have been flushed by someone else.
+ *
  * Returns true if the buffer was valid and it has now been made invalid.
  * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
 {
 	BufferDesc *desc;
-	uint32		buf_state;
-	bool		result;
+
+	Assert(BufferIsValid(buf) && !BufferIsLocal(buf));
 
 	/* Make sure we can pin the buffer. */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
 	ReservePrivateRefCountEntry();
 
-	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
+	LockBufHdr(desc);
 
-	/* 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;
-	}
+	return EvictUnpinnedBufferInternal(desc, flushed);
+}
 
-	/* Check that it's not pinned already. */
-	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+/*
+ * Try to evict all the shared buffers.
+ *
+ * This function is intended for testing/development use only! See
+ * EvictUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_evicted - were evicted
+ * - buffers_flushed - were flushed
+ * - buffers_skipped - could not be evicted
+ */
+void
+EvictAllUnpinnedBuffers(int32 *buffers_evicted, int32 *buffers_flushed,
+						int32 *buffers_skipped)
+{
+	*buffers_evicted = 0;
+	*buffers_skipped = 0;
+	*buffers_flushed = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
 	{
-		UnlockBufHdr(desc, buf_state);
-		return false;
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state;
+		bool		flushed;
+
+		buf_state = pg_atomic_read_u32(&desc->state);
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		if (EvictUnpinnedBufferInternal(desc, &flushed))
+			(*buffers_evicted)++;
+		else
+			(*buffers_skipped)++;
+
+		if (flushed)
+			(*buffers_flushed)++;
 	}
+}
+
+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only! See
+ * EvictUnpinnedBuffer().
+ *
+ * The caller must hold at least AccessShareLock on the relation to prevent
+ * the relation from being dropped.
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_evicted - were evicted
+ * - buffers_flushed - were flushed
+ * - buffers_skipped - could not be evicted
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
+						int32 *buffers_flushed, int32 *buffers_skipped)
+{
+	Assert(!RelationUsesLocalBuffers(rel));
 
-	PinBuffer_Locked(desc);		/* releases spinlock */
+	*buffers_skipped = 0;
+	*buffers_evicted = 0;
+	*buffers_flushed = 0;
 
-	/* If it was dirty, try to clean it once. */
-	if (buf_state & BM_DIRTY)
+	for (int buf = 1; buf <= NBuffers; buf++)
 	{
-		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
-		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
-		LWLockRelease(BufferDescriptorGetContentLock(desc));
-	}
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state = pg_atomic_read_u32(&(desc->state));
+		bool		flushed;
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+			continue;
 
-	/* This will return false if it becomes dirty or someone else pins it. */
-	result = InvalidateVictimBuffer(desc);
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
 
-	UnpinBuffer(desc);
+		buf_state = LockBufHdr(desc);
 
-	return result;
+		/* 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 (EvictUnpinnedBufferInternal(desc, &flushed))
+			(*buffers_evicted)++;
+		else
+			(*buffers_skipped)++;
+
+		if (flushed)
+			(*buffers_flushed)++;
+	}
 }
 
 /*
diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index b745dc69eae..ba6fc14044d 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -55,3 +55,67 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
  t
 (1 row)
 
+RESET role;
+------
+---- Test pg_buffercache_evict* functions
+------
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+ERROR:  must be superuser to use pg_buffercache_evict()
+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()
+RESET ROLE;
+-- These should return nothing, because these are STRICT functions
+SELECT * FROM pg_buffercache_evict(NULL);
+ evicted | flushed 
+---------+---------
+         | 
+(1 row)
+
+SELECT * FROM pg_buffercache_evict_relation(NULL);
+ buffers_evicted | buffers_flushed | 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
+SELECT * FROM pg_buffercache_evict(-1);
+ERROR:  bad buffer ID: -1
+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
+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
+DROP TABLE temp_pg_buffercache;
+-- These shouldn't fail
+SELECT evicted IS NOT NULL FROM pg_buffercache_evict(1);
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_all();
+ ?column? 
+----------
+ t
+(1 row)
+
+CREATE TABLE shared_pg_buffercache();
+SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE shared_pg_buffercache;
+DROP ROLE regress_buffercache_normal;
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index f6668e41b37..b7ac7daecae 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -20,3 +20,27 @@ REVOKE ALL ON pg_buffercache_numa FROM PUBLIC;
 
 GRANT EXECUTE ON FUNCTION pg_buffercache_numa_pages() TO pg_monitor;
 GRANT SELECT ON pg_buffercache_numa TO pg_monitor;
+
+
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_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 a702a47efe9..6cff2b5f8c1 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,17 +9,22 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "port/pg_numa.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3
 
 #define NUM_BUFFERCACHE_NUMA_ELEM	3
 
@@ -93,6 +98,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_numa_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 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);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -637,21 +644,131 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 	return (Datum) 0;
 }
 
+/*
+ * Helper function to check if the user has superuser privileges.
+ */
+static void
+pg_buffercache_superuser_check(char *func_name)
+{
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						func_name)));
+}
+
 /*
  * Try to evict a shared buffer.
  */
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
+
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
 
-	if (!superuser())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("must be superuser to use pg_buffercache_evict function")));
+	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_evict");
 
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 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_evict_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_evict_relation")));
+
+	EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed,
+							&buffers_skipped);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 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_evict_all");
+
+	EvictAllUnpinnedBuffers(&buffers_evicted, &buffers_flushed,
+							&buffers_skipped);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+	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 944fbb1beae..21dd666199c 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -26,3 +26,45 @@ SET ROLE pg_monitor;
 SELECT count(*) > 0 FROM pg_buffercache;
 SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary();
 SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
+RESET role;
+
+
+------
+---- Test pg_buffercache_evict* functions
+------
+
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+SELECT * FROM pg_buffercache_evict_relation(1);
+SELECT * FROM pg_buffercache_evict_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);
+
+-- 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
+SELECT * FROM pg_buffercache_evict(-1);
+SELECT * FROM pg_buffercache_evict(0);
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+DROP TABLE temp_pg_buffercache;
+
+-- These shouldn't fail
+SELECT 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');
+DROP TABLE shared_pg_buffercache;
+
+DROP ROLE regress_buffercache_normal;
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index b5050cd7343..1585c7cdf75 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,14 +27,24 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   <function>pg_buffercache_numa_pages()</function> function (wrapped in the
   <structname>pg_buffercache_numa</structname> view), the
   <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <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.
  </para>
 
  <para>
@@ -76,6 +86,19 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  unpinned shared buffers in the relation to be evicted from the buffer pool
+  given a relation identifier.  Use of this function is restricted to
+  superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  unpinned shared buffers to be evicted 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>
 
@@ -452,11 +475,46 @@
   <para>
    The <function>pg_buffercache_evict()</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, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   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
+   the <structname>pg_buffercache</structname> view.  It returns information
+   about whether the buffer was evicted and flushed.  The evicted column is
+   true on success, and false if the buffer wasn't valid, if it couldn't be
+   evicted because it was pinned, or if it became dirty again after an attempt
+   to write it out.  The flushed column is true if the buffer was flushed.
+   This does not necessarily mean that buffer was flushed by us, it might be
+   flushed by someone else.  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-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  It tries to
+   evict all buffers for all forks in that relation.  It returns the number of
+   evicted buffers, flushed buffers and the number of buffers that could not
+   be evicted.  Flushed buffers haven't necessarily been flushed by us, they
+   might have been flushed by someone else.  The result is immediately out of
+   date upon return, as buffers might immediately be read back in due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted buffers, flushed buffers and
+   the number of buffers that could not be evicted.  Flushed buffers haven't
+   necessarily been flushed by us, they might have been flushed by someone
+   else.  The result is immediately out of date upon return, as buffers might
+   immediately be read back in due to concurrent activity.  The function is
    intended for developer testing only.
   </para>
  </sect2>
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index bef0ecd9007..1d776010ef4 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -203,6 +203,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	bool		corrupt_header = PG_GETARG_BOOL(3);
 	bool		corrupt_checksum = PG_GETARG_BOOL(4);
 	Page		page = palloc_aligned(BLCKSZ, PG_IO_ALIGN_SIZE, 0);
+	bool		flushed;
 	Relation	rel;
 	Buffer		buf;
 	PageHeader	ph;
@@ -237,7 +238,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	if (BufferIsLocal(buf))
 		InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
 	else
-		EvictUnpinnedBuffer(buf);
+		EvictUnpinnedBuffer(buf, &flushed);
 
 	/*
 	 * Now modify the page as asked for by the caller.
@@ -478,6 +479,7 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 			BufferDesc *buf_hdr = BufferIsLocal(buf) ?
 				GetLocalBufferDescriptor(-buf - 1)
 				: GetBufferDescriptor(buf - 1);
+			bool		flushed;
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -493,7 +495,7 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 
 			if (BufferIsLocal(buf))
 				InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
-			else if (!EvictUnpinnedBuffer(buf))
+			else if (!EvictUnpinnedBuffer(buf, &flushed))
 				elog(ERROR, "couldn't evict");
 		}
 	}
-- 
2.48.1.76.g4e746b1a31.dirty

