From 8cf16c7d4db804442a4b420c393443002bfaa06d Mon Sep 17 00:00:00 2001
From: Dmitrii Dolgov <9erthalion6@gmail.com>
Date: Tue, 17 Jun 2025 14:16:55 +0200
Subject: [PATCH v20260209 3/7] Allow to resize shared memory without restart

shared_buffers is now PGC_SIGHUP instead of PGC_POSTMASTER.  The value
of this GUC is saved in NBuffersPending instead of NBuffers. When the
server starts, the shared memory size is estimated and the memory is
allocated using NBuffersPending.

When a server is running, the new value of GUC (set using ALTER SYSTEM
... SET shared_buffers = ...; followed by SELECT pg_reload_conf()) does
not come into effect immediately. Instead a function
pg_resize_shared_buffers() is used to resize the buffer pool. The
function uses the current value of GUC in the backends where it is
executed. The function also coordinates the buffer resizing
synchronization across backends.

SHOW shared_buffers now shows the current size of the shared buffer pool
but it also shows pending size of shared buffers, if any.

A new GUC max_shared_buffers is introduced to control the maximum value
of shared_buffers that can be set. By default it is 0. When explicitly
set it needs to be higher than 'shared_buffers'. When max_shared_buffers
is set to 0, it assumes the same value as GUC shared_buffers. This GUC
determines the size of address space reserved for future buffer pool
sizes and the size of buffer look up table.

TBD: Describe the protocol used by pg_resize_shared_buffers() to
synchronize buffer resizing operation with other backends.

When shrinking the shared buffers pool, each buffer in the area being
shrunk needs to be flushed if it's dirty so as not to loose the changes
to that buffer after shrinking. Also, each such buffer needs to be
removed from the buffer mapping table so that backends do not access it
after shrinking.

If a buffer being evicted is pinned, we abort the resizing operation.
There are other alternative which are not implemented in the current
patches 1. to wait for the pinned buffer to get unpinned, 2. the backend
is killed or it itself cancels the query  or 3.  rollback the operation.
Note that option 1 and 2 would require the pinning related local and
shared records to be accessed. But we need infrastructure to do either
of this right now.

So far the buffer pool metdata (NBuffers and the shared memory segment
address space) is saved in process local heap memory since it's static
for the life of a server. It is passed to a new backend through
Postmaster. But with buffer pool being resized while the server running,
we need Postmaster to update its buffer pool metadata as the resizing
progresses and pass it to the new backend. This has few complications:

1. Postmaster does not receive ProcSignalBarrier. So we need to signal
   it separately.

2. Postmaster's local state is inherited by the new backend when
   fork()ed. But we need more complex implementation to pass it to an
   exec()ed backend.

3. A new backend may receive the updated state from Postmaster and also
   the signal barrier which prompts the same update. Thus the proc
   signal barrier code needs to be idempotent; adding further complexity
   to it.

4. This task takes away Postmaster resources from it's core
   functionality.

This can be avoided by following two changes:
1. The shared memory is resized only in a single backend without
   requiring any changes to the memory address space.
2. Maintaining the buffer pool metadata in the shared memory instead of
   process local memory. This change may affect performance so verify
   that performance is not degraded.

TODO: In case the backend executing pg_resize_shared_buffers() exits
before the operation finishes, we need to make sure that the changes
made to the shared memory while resizing are cleaned up properly.

Removing the evicted buffers from buffer ring
=============================================
If the buffer pool has been shrunk, the buffers in the buffer ring may
not be valid anymore. Modify GetBufferFromRing to check if the buffer is
still valid before using it. This makes GetBufferFromRing() a bit more
expensive because of additional boolean condition and masks any bug that
introduces an invalid buffer into the ring. The alternative fix is more
complex as explained below.

The strategy object is created in CurrentMemoryContext and is not
available in any global structure thus accessible when processing buffer
resizing barriers. We may modify GetAccessStrategy() to register
strategy in a global linked list and then arrange to deregister it once
it's no more in use. Looking at the places which use
GetAccessStrategy(), fixing all those may be some work.

Author: Ashutosh Bapat
Author: Dmitrii Dolgov
Author of some tests: Palak Chaturvedi <chaturvedipalak1911@gmail.com>
Reviewed-by: Tomas Vondra

More detailed note follow: Need to see which of those fit in the commit
message and which should be removed.

Reinitializing strategry control area
=====================================
The commit introduces a separate function StrategyReInitialize() instead
of reusing StrategyInitialize() since some of the things that the second
one does are not required in the first one. Here's list of what
StrategyReInitialize() does and how does it differ from
StrategyInitialize().

1. StrategyControl pointer needn't be fetched again since it should not
   change. But added an Assert to make sure the pointer is valid.
2. &StrategyControl->buffer_strategy_lock need not be initialized again.
3. nextVictimBuffer, completePasses and numBufferAllocs are viewed in
   the context of NBuffers. Now that NBuffers itself has changed, those
   three do not make sense. Reset them as if the server has restarted
   again.

Ability to delay resizing operation
===================================
This commit introduces a flag delay_shmem_resize, which postgresql
backends and workers can use to signal the coordinator to delay resizing
operation. Background writer sets this flag when its scanning buffers.

Background writer operation (needs a rethink)
===========================
Background writer is blocked when the actual resizing is in progress. It
stops a scan in progress when it sees that the resizing has begun or is
about to begin. Once the buffer resizing is finished, before resuming
the regular operation, bgwriter resets the information saved so far.
This information is viewed in the context of NBuffers and hence does not
make sense after resizing which chanegs NBuffers.

Buffer lookup table
===================
Right now there is no way to free shared memory. Even if we shrink the
buffer lookup table when shrinking the buffer pool the unused hash table
entries can not be freed. When we expand the buffer pool, more entries
can be allocated but we can not resize the hash table directory without
rehashing all the entries. Just allocating more entries will lead to
more contention. Hence we setup the buffer lookup table considering the
maximum possible size of the buffer pool which is MaxAvailableMemory
only once at the beginning.  Shared buffer lookup table and
StrategyControl are not resized even if the buffer pool is resized hence
they are allocated in the main shared memory segment

BgWriter refactoring
====================

The way BgBufferSync is written today, it packs four functionalities:
setting up the buffer sync state, performing the buffer sync, resetting
the buffer sync state when bgwriter_lru_maxpages <= 0 and setting it up
again after bgwriter_lru_maxpages > 0. That makes the code hard to read.
It will be good to divide this function into 3/4 different functions
each performing one functionality. Then pack all the state (the local
variables from that function converted to static global) into a
structure, which is passed to these functions. Once that happens
BgBufferSyncReset() will call one of the functions to reset the state
when buffer pool is resized.
---
 contrib/pg_buffercache/pg_buffercache_pages.c |  18 +-
 doc/src/sgml/config.sgml                      |  45 +-
 doc/src/sgml/func/func-admin.sgml             |  57 +++
 src/backend/access/transam/slru.c             |   2 +-
 src/backend/access/transam/xlog.c             |   2 +-
 src/backend/bootstrap/bootstrap.c             |   2 +
 src/backend/optimizer/path/costsize.c         |   8 +-
 src/backend/port/sysv_shmem.c                 | 164 +++++++
 src/backend/port/win32_shmem.c                |   6 +
 src/backend/postmaster/checkpointer.c         |  23 +-
 src/backend/postmaster/postmaster.c           |  10 +-
 src/backend/storage/aio/aio_init.c            |   2 +-
 src/backend/storage/buffer/Makefile           |   3 +-
 src/backend/storage/buffer/README             |  74 +++
 src/backend/storage/buffer/buf_init.c         | 220 +++++++--
 src/backend/storage/buffer/buf_resize.c       | 455 ++++++++++++++++++
 src/backend/storage/buffer/buf_table.c        |  10 +-
 src/backend/storage/buffer/bufmgr.c           | 178 ++++++-
 src/backend/storage/buffer/freelist.c         | 101 +++-
 src/backend/storage/buffer/meson.build        |   1 +
 src/backend/storage/ipc/ipci.c                |  13 +-
 src/backend/storage/ipc/procsignal.c          |  56 +++
 src/backend/storage/ipc/shmem.c               | 103 +++-
 src/backend/tcop/postgres.c                   |  11 +
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/globals.c              |   5 +-
 src/backend/utils/init/postinit.c             |  49 ++
 src/backend/utils/misc/guc.c                  |   2 +-
 src/backend/utils/misc/guc_parameters.dat     |  15 +-
 src/include/catalog/pg_proc.dat               |   6 +
 src/include/miscadmin.h                       |   8 +
 src/include/storage/buf.h                     |   2 +-
 src/include/storage/buf_internals.h           |   5 +-
 src/include/storage/bufmgr.h                  |  20 +-
 src/include/storage/ipc.h                     |   1 +
 src/include/storage/lwlocklist.h              |   1 +
 src/include/storage/pg_shmem.h                |  62 ++-
 src/include/storage/procsignal.h              |   9 +
 src/include/storage/shmem.h                   |   3 +
 src/include/utils/guc.h                       |   2 +
 src/test/Makefile                             |   3 +-
 src/test/README                               |   3 +
 src/test/buffermgr/Makefile                   |  30 ++
 src/test/buffermgr/README                     |  26 +
 src/test/buffermgr/buffermgr_test.conf        |  11 +
 src/test/buffermgr/expected/buffer_resize.out | 312 ++++++++++++
 src/test/buffermgr/meson.build                |  23 +
 src/test/buffermgr/sql/buffer_resize.sql      |  97 ++++
 src/test/buffermgr/t/001_resize_buffer.pl     | 143 ++++++
 .../buffermgr/t/003_parallel_resize_buffer.pl |  71 +++
 .../t/004_client_join_buffer_resize.pl        | 243 ++++++++++
 src/test/meson.build                          |   1 +
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    |  76 +++
 src/tools/pgindent/typedefs.list              |   1 +
 54 files changed, 2685 insertions(+), 111 deletions(-)
 create mode 100644 src/backend/storage/buffer/buf_resize.c
 create mode 100644 src/test/buffermgr/Makefile
 create mode 100644 src/test/buffermgr/README
 create mode 100644 src/test/buffermgr/buffermgr_test.conf
 create mode 100644 src/test/buffermgr/expected/buffer_resize.out
 create mode 100644 src/test/buffermgr/meson.build
 create mode 100644 src/test/buffermgr/sql/buffer_resize.sql
 create mode 100644 src/test/buffermgr/t/001_resize_buffer.pl
 create mode 100644 src/test/buffermgr/t/003_parallel_resize_buffer.pl
 create mode 100644 src/test/buffermgr/t/004_client_join_buffer_resize.pl

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index f60f797a9b4..8a17319ff2a 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -125,6 +125,7 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 	TupleDesc	tupledesc;
 	TupleDesc	expected_tupledesc;
 	HeapTuple	tuple;
+	int			currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers);
 
 	if (SRF_IS_FIRSTCALL())
 	{
@@ -181,10 +182,10 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		/* Allocate NBuffers worth of BufferCachePagesRec records. */
 		fctx->record = (BufferCachePagesRec *)
 			MemoryContextAllocHuge(CurrentMemoryContext,
-								   sizeof(BufferCachePagesRec) * NBuffers);
+								   sizeof(BufferCachePagesRec) * currentNBuffers);
 
 		/* Set max calls and remember the user function context. */
-		funcctx->max_calls = NBuffers;
+		funcctx->max_calls = currentNBuffers;
 		funcctx->user_fctx = fctx;
 
 		/* Return to original context when allocating transient memory */
@@ -198,13 +199,24 @@ pg_buffercache_pages(PG_FUNCTION_ARGS)
 		 * snapshot across all buffers, but we do grab the buffer header
 		 * locks, so the information of each buffer is self-consistent.
 		 */
-		for (i = 0; i < NBuffers; i++)
+		for (i = 0; i < currentNBuffers; i++)
 		{
 			BufferDesc *bufHdr;
 			uint64		buf_state;
 
 			CHECK_FOR_INTERRUPTS();
 
+			/*
+			 * TODO: We should just scan the entire buffer descriptor array
+			 * instead of relying on curent buffer pool size. But that can
+			 * happen if only we setup the descriptor array large enough at
+			 * the server startup time.
+			 */
+			if (currentNBuffers != pg_atomic_read_u32(&ShmemCtrl->currentNBuffers))
+				ereport(ERROR,
+						(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						 errmsg("number of shared buffers changed during scan of buffer cache")));
+
 			bufHdr = GetBufferDescriptor(i);
 			/* Lock each buffer header before inspecting. */
 			buf_state = LockBufHdr(bufHdr);
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f1af1505cf3..71654f94ce8 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1732,7 +1732,6 @@ include_dir 'conf.d'
         that is <symbol>BLCKSZ</symbol> bytes, typically 8kB.
         (Non-default values of <symbol>BLCKSZ</symbol> change the minimum
         value.)
-        This parameter can only be set at server start.
        </para>
 
        <para>
@@ -1755,6 +1754,50 @@ include_dir 'conf.d'
         appropriate, so as to leave adequate space for the operating system.
        </para>
 
+       <para>
+        The shared memory consumed by the buffer pool is allocated and
+        initialized according to the value of the GUC at the time of starting
+        the server. A desired new value of GUC can be loaded while the server is
+        running using <systemitem>SIGHUP</systemitem>. But the buffer pool will
+        not be resized immediately. Use
+        <function>pg_resize_shared_buffers()</function> to dynamically resize
+        the shared buffer pool (see <xref linkend="functions-admin"/> for details).
+        <command>SHOW shared_buffers</command> shows the current number of
+        shared buffers and pending number, if any. Please note that when the GUC
+        is changed, the other GUCS which use this GUCs value to set their
+        defaults will not be changed. They may still require a server restart to
+        consider new value.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-max-shared-buffers" xreflabel="max_shared_buffers">
+      <term><varname>max_shared_buffers</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>max_shared_buffers</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the upper limit for the <varname>shared_buffers</varname> value.
+        The default value is <literal>0</literal>,
+        which means no explicit limit is set and <varname>max_shared_buffers</varname>
+        will be automatically set to the value of <varname>shared_buffers</varname>
+        at server startup.
+        If this value is specified without units, it is taken as blocks,
+        that is <symbol>BLCKSZ</symbol> bytes, typically 8kB.
+        This parameter can only be set at server start.
+       </para>
+
+       <para>
+        This parameter determines the amount of memory address space to reserve
+        in each backend for expanding the buffer pool in future. While the
+        memory for buffer pool is allocated on demand as it is resized, the
+        memory required to hold the buffer manager metadata is allocated
+        statically at the server start accounting for the largest buffer pool
+        size allowed by this parameter.
+        <!-- TODO: Provide a numeric example of how much extra memory say max_shared_buffers = 1GB consume. -->
+       </para>
       </listitem>
      </varlistentry>
 
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 3ac81905d1f..22bae4e0bac 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -99,6 +99,63 @@
         <returnvalue>off</returnvalue>
        </para></entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_resize_shared_buffers</primary>
+        </indexterm>
+        <function>pg_resize_shared_buffers</function> ()
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Dynamically resizes the shared buffer pool to match the current
+        value of the <varname>shared_buffers</varname> parameter. This
+        function implements a coordinated resize process that ensures all
+        backend processes acknowledge the change before completing the
+        operation. The resize happens in multiple phases to maintain
+        data consistency and system stability. Returns <literal>true</literal>
+        if the resize was successful, or raises an error if the operation
+        fails. This function can only be called by superusers.
+       </para>
+       <para>
+        To resize shared buffers, first update the <varname>shared_buffers</varname>
+        setting and reload the configuration, then verify the new value is loaded
+        before calling this function. For example:
+<programlisting>
+postgres=# ALTER SYSTEM SET shared_buffers = '256MB';
+ALTER SYSTEM
+postgres=# SELECT pg_reload_conf();
+ pg_reload_conf
+----------------
+ t
+(1 row)
+
+postgres=# SHOW shared_buffers;
+     shared_buffers
+-------------------------
+ 128MB (pending: 256MB)
+(1 row)
+
+postgres=# SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers
+--------------------------
+ t
+(1 row)
+
+postgres=# SHOW shared_buffers;
+ shared_buffers
+----------------
+ 256MB
+(1 row)
+</programlisting>
+        The <command>SHOW shared_buffers</command> step is important to verify
+        that the configuration reload was successful and the new value is
+        available to the current session before attempting the resize. The
+        output shows both the current and pending values when a change is waiting
+        to be applied.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c
index 549c7e3e64b..2f30be34731 100644
--- a/src/backend/access/transam/slru.c
+++ b/src/backend/access/transam/slru.c
@@ -232,7 +232,7 @@ SimpleLruAutotuneBuffers(int divisor, int max)
 {
 	return Min(max - (max % SLRU_BANK_SIZE),
 			   Max(SLRU_BANK_SIZE,
-				   NBuffers / divisor - (NBuffers / divisor) % SLRU_BANK_SIZE));
+				   NBuffersPending / divisor - (NBuffersPending / divisor) % SLRU_BANK_SIZE));
 }
 
 /*
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 13ec6225b85..b2605dbf87a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4694,7 +4694,7 @@ XLOGChooseNumBuffers(void)
 {
 	int			xbuffers;
 
-	xbuffers = NBuffers / 32;
+	xbuffers = NBuffersPending / 32;
 	if (xbuffers > (wal_segment_size / XLOG_BLCKSZ))
 		xbuffers = (wal_segment_size / XLOG_BLCKSZ);
 	if (xbuffers < 8)
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 7d32cd0e159..638940363b4 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -337,6 +337,8 @@ BootstrapModeMain(int argc, char *argv[], bool check_only)
 
 	InitializeFastPathLocks();
 
+	InitializeMaxNBuffers();
+
 	CreateSharedMemoryAndSemaphores();
 
 	/*
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index c30d6e84672..23d87bdb3c9 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -19,10 +19,10 @@
  * is normally considerably less than random_page_cost.  (However, if the
  * database is fully cached in RAM, it is reasonable to set them equal.)
  *
- * We also use a rough estimate "effective_cache_size" of the number of
- * disk pages in Postgres + OS-level disk cache.  (We can't simply use
- * NBuffers for this purpose because that would ignore the effects of
- * the kernel's disk cache.)
+ * We also use a rough estimate "effective_cache_size" of the number of disk
+ * pages in Postgres + OS-level disk cache.  (We can't simply use size of the
+ * buffer pool for this purpose because that would ignore the effects of the
+ * kernel's disk cache.)
  *
  * Obviously, taking constants for these values is an oversimplification,
  * but it's tough enough to get any useful estimates even at this level of
diff --git a/src/backend/port/sysv_shmem.c b/src/backend/port/sysv_shmem.c
index 29ffcaa35f3..c766f5c44fd 100644
--- a/src/backend/port/sysv_shmem.c
+++ b/src/backend/port/sysv_shmem.c
@@ -30,14 +30,19 @@
 #include "miscadmin.h"
 #include "port/pg_bitutils.h"
 #include "portability/mem.h"
+#include "storage/bufmgr.h"
 #include "storage/dsm.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "storage/lwlock.h"
 #include "storage/pg_shmem.h"
+#include "storage/pmsignal.h"
+#include "storage/procsignal.h"
 #include "storage/shmem.h"
 #include "utils/guc.h"
 #include "utils/guc_hooks.h"
 #include "utils/pidfile.h"
+#include "utils/wait_event.h"
 
 /*
  * TODO: The first two sentences in the first paragraph below make me feel like
@@ -101,6 +106,8 @@ typedef enum
 	SHMSTATE_UNATTACHED,		/* pertinent to DataDir, no attached PIDs */
 } IpcMemoryState;
 
+volatile bool delay_shmem_resize = false;
+
 /*
  * Anonymous mapping layout we use looks like this:
  *
@@ -127,6 +134,9 @@ typedef enum
  * being counted against memory limits). The mapping serves as an address space
  * reservation, into which shared memory segment can be extended and is
  * represented by the second /memfd:main with no permissions.
+ *
+ * The reserved space for buffer manager related segments is calculated based on
+ * MaxNBuffers.
  */
 
 PGUsedShmemInfo UsedShmemInfo[NUM_MEMORY_MAPPINGS];
@@ -152,6 +162,42 @@ AnonShmemData AnonShmemInfo[NUM_MEMORY_MAPPINGS];
  */
 static bool huge_pages_on = false;
 
+/*
+ * Currently broadcasted value of NBuffers in shared memory.
+ *
+ * Most of the time this value is going to be equal to NBuffers. But if
+ * postmaster is resizing shared memory and a new backend was created
+ * at the same time, there is a possibility for the new backend to inherit the
+ * old NBuffers value, but miss the resize signal if ProcSignal infrastructure
+ * was not initialized yet. Consider this situation:
+ *
+ *     Postmaster ------> New Backend
+ *         |                   |
+ *         |                Launch
+ *         |                   |
+ *         |             Inherit NBuffers
+ *         |                   |
+ *     Resize NBuffers         |
+ *         |                   |
+ *     Emit Barrier            |
+ *         |            Init ProcSignal
+ *         |                   |
+ *     Finish resize           |
+ *         |                   |
+ *     New NBuffers       Old NBuffers
+ *
+ * In this case the backend is not yet ready to receive a signal from
+ * EmitProcSignalBarrier, and will be ignored. The same happens if ProcSignal
+ * is initialized even later, after the resizing was finished.
+ *
+ * To address resulting inconsistency, postmaster broadcasts the current
+ * NBuffers value via shared memory. Every new backend has to verify this value
+ * before it will access the buffer pool: if it differs from its own value,
+ * this indicates a shared memory resize has happened and the backend has to
+ * first synchronize with rest of the pack.
+ */
+ShmemControl *ShmemCtrl = NULL;
+
 static void *InternalIpcMemoryCreate(IpcMemoryKey memKey, Size size);
 static void IpcMemoryDetach(int status, Datum shmaddr);
 static void IpcMemoryDelete(int status, Datum shmId);
@@ -946,6 +992,70 @@ AnonymousShmemDetach(int status, Datum arg)
 	}
 }
 
+/*
+ * Resize all shared memory segments based on the new shared_buffers value (saved
+ * in ShmemCtrl area). The actual segment resizing is done via ftruncate, which
+ * will fail if there is not sufficient space to expand the anon file.
+ *
+ * TODO: Rename this to BufferShmemResize() or something. Only buffer manager's
+ * memory should be resized in this function.
+ *
+ * TODO: This function changes the amount of shared memory used. So it should
+ * also update the show only GUCs shared_memory_size and
+ * shared_memory_size_in_huge_pages in all backends. SetConfigOption() may be
+ * used for that. But it's not clear whether is_reload parameter is safe to use
+ * while resizing is going on; also at what stage it should be done.
+ */
+static bool
+AnonymousShmemResize(int segment_id, MemoryMappingSizes *mapping, bool expanding)
+{
+	Size		hugepagesize;
+	AnonShmemData *anonshmem = &AnonShmemInfo[segment_id];
+
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	elog(DEBUG1, "Resize shmem from %d to %d", NBuffers, NBuffersPending);
+
+	if (anonshmem->fd == -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("segment[%s]: only anonymous (mmaped) file backed segments can be resized",
+						MappingName(segment_id))));
+
+#ifndef MAP_HUGETLB
+	/* PrepareHugePages should have dealt with this case */
+	Assert(huge_pages != HUGE_PAGES_ON && !huge_pages_on);
+#else
+	if (huge_pages_on)
+	{
+		Assert(huge_pages == HUGE_PAGES_ON || huge_pages == HUGE_PAGES_TRY);
+		GetHugePageSize(&hugepagesize, NULL, NULL);
+		round_off_mapping_sizes_for_hugepages(mapping, hugepagesize);
+	}
+#endif
+	Assert(anonshmem->addr);
+
+	/*
+	 * Size of the reserved address space should not change, since it depends
+	 * upon MaxNBuffers, which can be changed only on restart.
+	 */
+	Assert(anonshmem->size == mapping->shmem_reserved);
+
+	/*
+	 * Resize the backing file to resize the allocated memory, and allocate
+	 * more memory on supported platforms if required.
+	 */
+	if (ftruncate(anonshmem->fd, mapping->shmem_req_size) == -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYSTEM_ERROR),
+				 errmsg("could not truncate anonymous file segment for \"%s\": %m",
+						MappingName(segment_id))));
+	if (expanding)
+		shmem_fallocate(anonshmem->fd, MappingName(segment_id), mapping->shmem_req_size, ERROR);
+
+	return true;
+}
+
 /*
  * PGSharedMemoryCreate
  *
@@ -1130,6 +1240,41 @@ PGSharedMemoryCreate(int segment_id, MemoryMappingSizes *mapping,
 	return anonshmem->addr;
 }
 
+bool
+PGSharedMemoryResize(int segment_id, MemoryMappingSizes *mapping)
+{
+	AnonShmemData *anonshmem = &AnonShmemInfo[segment_id];
+	PGShmemHeader *hdr;
+
+	/* For now, we allow only mmapped memory to be resized. */
+	if (shared_memory_type != SHMEM_TYPE_MMAP || anonshmem->fd == -1)
+		elog(ERROR, "only anonymous (mmaped) file backed memory can be resized");
+
+	/* Anonymous memory has header as the first chunk. */
+	hdr = (PGShmemHeader *) anonshmem->addr;
+	/* Main shared memory segment is always static. */
+	Assert(segment_id != MAIN_SHMEM_SEGMENT);
+
+	/*
+	 * We should have reserved enough address space for resizing. PANIC if
+	 * that's not the case.
+	 */
+	if (hdr->reservedsize < mapping->shmem_req_size)
+		ereport(PANIC,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("not enough shared memory is reserved")));
+
+	/* Nothing to do if size is unchanged */
+	if (hdr->totalsize == mapping->shmem_req_size)
+		return true;
+
+	AnonymousShmemResize(segment_id, mapping, mapping->shmem_req_size > hdr->totalsize);
+
+	/* Update the available size. */
+	hdr->totalsize = mapping->shmem_req_size;
+	return true;
+}
+
 #ifdef EXEC_BACKEND
 
 /*
@@ -1271,3 +1416,22 @@ PGSharedMemoryDetach(void)
 		}
 	}
 }
+
+void
+ShmemControlInit(void)
+{
+	bool		foundShmemCtrl;
+
+	ShmemCtrl = (ShmemControl *)
+		ShmemInitStruct("Shmem Control", sizeof(ShmemControl),
+						&foundShmemCtrl);
+
+	if (!foundShmemCtrl)
+	{
+		pg_atomic_init_u32(&ShmemCtrl->targetNBuffers, 0);
+		pg_atomic_init_u32(&ShmemCtrl->currentNBuffers, 0);
+		pg_atomic_init_flag(&ShmemCtrl->resize_in_progress);
+
+		ShmemCtrl->coordinator = 0;
+	}
+}
diff --git a/src/backend/port/win32_shmem.c b/src/backend/port/win32_shmem.c
index 034460eab96..3601d13d1cf 100644
--- a/src/backend/port/win32_shmem.c
+++ b/src/backend/port/win32_shmem.c
@@ -415,6 +415,12 @@ retry:
 					"on" : "off", PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
 }
 
+bool
+PGSharedMemoryResize(int segment_id, MemoryMappingSizes *mapping)
+{
+	elog(ERROR, "shared memory resizing is not supported on Windows");
+}
+
 /*
  * PGSharedMemoryReAttach
  *
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index e03c19123bc..53282da5e25 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -659,9 +659,12 @@ CheckpointerMain(const void *startup_data, size_t startup_data_len)
 static void
 ProcessCheckpointerInterrupts(void)
 {
-	if (ProcSignalBarrierPending)
-		ProcessProcSignalBarrier();
-
+	/*
+	 * Reloading config can trigger further signals, complicating interrupts
+	 * processing -- so let it run first.
+	 *
+	 * XXX: Is there any need in memory barrier after ProcessConfigFile?
+	 */
 	if (ConfigReloadPending)
 	{
 		ConfigReloadPending = false;
@@ -681,6 +684,9 @@ ProcessCheckpointerInterrupts(void)
 		UpdateSharedMemoryConfig();
 	}
 
+	if (ProcSignalBarrierPending)
+		ProcessProcSignalBarrier();
+
 	/* Perform logging of memory contexts of this process */
 	if (LogMemoryContextPending)
 		ProcessLogMemoryContextInterrupt();
@@ -958,12 +964,13 @@ CheckpointerShmemSize(void)
 	Size		size;
 
 	/*
-	 * The size of the requests[] array is arbitrarily set equal to NBuffers.
-	 * But there is a cap of MAX_CHECKPOINT_REQUESTS to prevent accumulating
-	 * too many checkpoint requests in the ring buffer.
+	 * The size of the requests[] array is arbitrarily set equal to the
+	 * initial size of buffer pool.  But there is a cap of
+	 * MAX_CHECKPOINT_REQUESTS to prevent accumulating too many checkpoint
+	 * requests in the ring buffer.
 	 */
 	size = offsetof(CheckpointerShmemStruct, requests);
-	size = add_size(size, mul_size(Min(NBuffers,
+	size = add_size(size, mul_size(Min(NBuffersPending,
 									   MAX_CHECKPOINT_REQUESTS),
 								   sizeof(CheckpointerRequest)));
 
@@ -994,7 +1001,7 @@ CheckpointerShmemInit(void)
 		 */
 		MemSet(CheckpointerShmem, 0, size);
 		SpinLockInit(&CheckpointerShmem->ckpt_lck);
-		CheckpointerShmem->max_requests = Min(NBuffers, MAX_CHECKPOINT_REQUESTS);
+		CheckpointerShmem->max_requests = Min(NBuffersPending, MAX_CHECKPOINT_REQUESTS);
 		CheckpointerShmem->head = CheckpointerShmem->tail = 0;
 		ConditionVariableInit(&CheckpointerShmem->start_cv);
 		ConditionVariableInit(&CheckpointerShmem->done_cv);
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d6133bfebc6..4c95376648d 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -110,11 +110,15 @@
 #include "replication/slotsync.h"
 #include "replication/walsender.h"
 #include "storage/aio_subsys.h"
+#include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/io_worker.h"
 #include "storage/ipc.h"
+#include "storage/pg_shmem.h"
 #include "storage/pmsignal.h"
 #include "storage/proc.h"
+#include "storage/procsignal.h"
+#include "storage/shmem.h"
 #include "tcop/backend_startup.h"
 #include "tcop/tcopprot.h"
 #include "utils/datetime.h"
@@ -125,7 +129,6 @@
 
 #ifdef EXEC_BACKEND
 #include "common/file_utils.h"
-#include "storage/pg_shmem.h"
 #endif
 
 
@@ -958,6 +961,11 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	InitializeFastPathLocks();
 
+	/*
+	 * Calculate MaxNBuffers for buffer pool resizing.
+	 */
+	InitializeMaxNBuffers();
+
 	/*
 	 * Give preloaded libraries a chance to request additional shared memory.
 	 */
diff --git a/src/backend/storage/aio/aio_init.c b/src/backend/storage/aio/aio_init.c
index d3c68d8b04c..b8dac7eb9d5 100644
--- a/src/backend/storage/aio/aio_init.c
+++ b/src/backend/storage/aio/aio_init.c
@@ -101,7 +101,7 @@ AioChooseMaxConcurrency(void)
 
 	/* Similar logic to LimitAdditionalPins() */
 	max_backends = MaxBackends + NUM_AUXILIARY_PROCS;
-	max_proportional_pins = NBuffers / max_backends;
+	max_proportional_pins = NBuffersPending / max_backends;
 
 	max_proportional_pins = Max(max_proportional_pins, 1);
 
diff --git a/src/backend/storage/buffer/Makefile b/src/backend/storage/buffer/Makefile
index fd7c40dcb08..3bc9aee85de 100644
--- a/src/backend/storage/buffer/Makefile
+++ b/src/backend/storage/buffer/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	buf_table.o \
 	bufmgr.o \
 	freelist.o \
-	localbuf.o
+	localbuf.o \
+	buf_resize.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index 119f31b5d65..5dd753718a0 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -261,3 +261,77 @@ As of 8.4, background writer starts during recovery mode when there is
 some form of potentially extended recovery to perform. It performs an
 identical service to normal processing, except that checkpoints it
 writes are technically restartpoints.
+
+Resizing shared buffers at runtime
+----------------------------------
+
+Before <TODO: Add version>, the size of the shared buffer pool (i.e. the number
+of shared buffers) was given by the global variable NBuffers and was fixed at
+server start time using GUC shared_buffers. In order to change the size of the
+shared buffer pool, one needed to change the GUC and restart the server.
+Starting <TODO: add version> PostgreSQL supports resizing the buffer pool
+without a server restart. The new GUC variable max_shared_buffers defines the
+maximum size of the shared buffer pool. Existing GUC shared_buffers controls the
+size of the buffer pool at run time. See configure.sgml for more details about
+these GUCs.
+
+The buffer manager maintains following data structures in shared memory.
+1. Buffer Descriptors: An array of BufferDesc structures, one per buffer.
+2. Buffer Blocks: An array of buffer blocks, forming the buffer pool.
+3. Buffer Lookup Table: A hash table mapping a page to the buffer containing
+   that page.
+4. IO conditional variables: An array of conditional variables, one per buffer.
+5. Checkpoint buffer ids: An array of buffer ids used during checkpointing.
+
+Except for the hash table, all the above data structures are required to be
+allocated as contiguous memory chunks. The code also relies on their start
+addresses being stable throughout the server lifetime. Resizing these structures
+means stretching or shrinking their tails. This is done by a. reserving address
+spaces for each of these structures during server start up, b. allocating or
+deallocating physical memory pages as needed during resizing, and c.
+initializing the array elements in the newly allocated memory regions.
+
+TODO: Should the memory management strategy be documented here or somewhere near
+storage/pg_shmem.c or sysv_shmem.c?
+
+Following protocol is used to make each of the segments elastic in a system
+which supports anonymous shared memory segments (like Linux). For each of the
+segments:
+1. Allocate a file descriptor using memfd_create() system call.
+2. Reserve address space for the segment using mmap(), passing it the fd
+   obtained in the first step with PROT_NONE and MAP_NORESERVE flags.
+3. Change the protection of the whole address space to PROT_READ | PROT_WRITE
+   using mprotect().
+4. Allocate or deallocate physical memory pages using ftruncate() as needed.
+
+The first three steps are executed when starting the server. The last step is
+executed when starting the server and during resizing.
+
+We should ideally call mmap() with PROT_READ | PROT_WRITE and avoid calling
+mprotect() separately. However, on Linux, when using huge pages, mmap() with
+PROT_READ | PROT_WRITE allocates memory for the whole region even if
+MAP_NORESERVE is specified. Above sequence avoids this problem.
+
+We could use mmap and mremap to resize the segments. However these calls will
+need to be executed by each backend process as well as the postmaster when
+resizing. This requires additional Synchronization between the postmaster and
+the resizing coordinator and requires careful implementation to avoid missing a
+newly joining backend during resizing. Using memfd_create and ftruncate avoids
+this complexity as ftruncate can be called from only one process, usually the
+coordinator and the changes are visible to all other processes automatically.
+
+Global variables (like NBuffers before this change) are inherited by each
+backend from the postmaster. If we continue to rely on NBuffers to provide the
+size of the buffer pool at a given point in time, we need to involve the
+postmater in resizing. To avoid this complexity, we store the current size of
+the buffer pool in the shared structure ShmemCtrl (name subject to change) and
+change only that variable during resizing. We use ProcSignalBarrier to know when
+all backends have observed the new size after resizing.
+
+See CreateAnonymousSegment() and AnonymousShmemResize() for detailed
+implementation.
+
+To resize shared buffers at runtime a user performs the steps mentioned in the
+description of shared_buffers GUC variable in configure.sgml. Actual resizing
+protocol is documented in the prologue of function pg_resize_shared_buffers() in
+src/backend/storage/buffer/buf_resize.c
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 42112109af9..d57c8bfb209 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -17,8 +17,8 @@
 #include "storage/aio.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
-#include "storage/pg_shmem.h"
 #include "storage/proclist.h"
+#include "utils/guc.h"
 
 BufferDescPadded *BufferDescriptors;
 char	   *BufferBlocks;
@@ -63,6 +63,23 @@ CkptSortItem *CkptBufferIds;
  * to allow dynamic resizing of the buffer pool.
  */
 
+/*
+ * Initialize a single buffer.
+ */
+static void
+InitializeBuffer(int buf_id)
+{
+	BufferDesc *buf = GetBufferDescriptor(buf_id);
+
+	ClearBufferTag(&buf->tag);
+	pg_atomic_init_u64(&buf->state, 0);
+	buf->wait_backend_pgprocno = INVALID_PROC_NUMBER;
+	buf->buf_id = buf_id;
+	pgaio_wref_clear(&buf->io_wref);
+	proclist_init(&buf->lock_waiters);
+	ConditionVariableInit(BufferDescriptorGetIOCV(buf));
+}
+
 
 /*
  * Initialize shared buffer pool
@@ -77,24 +94,25 @@ BufferManagerShmemInit(void)
 				foundDescs,
 				foundIOCV,
 				foundBufCkpt;
+	int			i;
 
 	/* Align descriptors to a cacheline boundary. */
 	BufferDescriptors = (BufferDescPadded *)
 		ShmemInitStructInSegment("Buffer Descriptors",
-								 NBuffers * sizeof(BufferDescPadded),
+								 MaxNBuffers * sizeof(BufferDescPadded),
 								 &foundDescs, MAIN_SHMEM_SEGMENT);
 
 	/* Align buffer pool on IO page size boundary. */
 	BufferBlocks = (char *)
 		TYPEALIGN(PG_IO_ALIGN_SIZE,
 				  ShmemInitStructInSegment("Buffer Blocks",
-										   NBuffers * (Size) BLCKSZ + PG_IO_ALIGN_SIZE,
+										   NBuffersPending * (Size) BLCKSZ + PG_IO_ALIGN_SIZE,
 										   &foundBufs, BUFFERS_SHMEM_SEGMENT));
 
 	/* Align condition variables to cacheline boundary. */
 	BufferIOCVArray = (ConditionVariableMinimallyPadded *)
 		ShmemInitStructInSegment("Buffer IO Condition Variables",
-								 NBuffers * sizeof(ConditionVariableMinimallyPadded),
+								 MaxNBuffers * sizeof(ConditionVariableMinimallyPadded),
 								 &foundIOCV, MAIN_SHMEM_SEGMENT);
 
 	/*
@@ -106,46 +124,41 @@ BufferManagerShmemInit(void)
 	 */
 	CkptBufferIds = (CkptSortItem *)
 		ShmemInitStructInSegment("Checkpoint BufferIds",
-								 NBuffers * sizeof(CkptSortItem), &foundBufCkpt,
+								 MaxNBuffers * sizeof(CkptSortItem), &foundBufCkpt,
 								 MAIN_SHMEM_SEGMENT);
 
 	if (foundDescs || foundBufs || foundIOCV || foundBufCkpt)
 	{
 		/* should find all of these, or none of them */
 		Assert(foundDescs && foundBufs && foundIOCV && foundBufCkpt);
-		/* note: this path is only taken in EXEC_BACKEND case */
+
+		/*
+		 * note: this path is only taken in EXEC_BACKEND case when
+		 * initializing shared memory.
+		 */
 	}
 	else
 	{
-		int			i;
-
 		/*
 		 * Initialize all the buffer headers.
 		 */
-		for (i = 0; i < NBuffers; i++)
-		{
-			BufferDesc *buf = GetBufferDescriptor(i);
-
-			ClearBufferTag(&buf->tag);
-
-			pg_atomic_init_u64(&buf->state, 0);
-			buf->wait_backend_pgprocno = INVALID_PROC_NUMBER;
-
-			buf->buf_id = i;
-
-			pgaio_wref_clear(&buf->io_wref);
-
-			proclist_init(&buf->lock_waiters);
-			ConditionVariableInit(BufferDescriptorGetIOCV(buf));
-		}
+		for (i = 0; i < NBuffersPending; i++)
+			InitializeBuffer(i);
 	}
 
-	/* Init other shared buffer-management stuff */
+	/*
+	 * Init other shared buffer-management stuff.
+	 */
 	StrategyInitialize(!foundDescs);
 
 	/* Initialize per-backend file flush context */
 	WritebackContextInit(&BackendWritebackContext,
 						 &backend_flush_after);
+
+	/* Declare the size of current buffer pool. */
+	NBuffers = NBuffersPending;
+	pg_atomic_init_u32(&ShmemCtrl->currentNBuffers, NBuffersPending);
+	pg_atomic_init_u32(&ShmemCtrl->targetNBuffers, NBuffersPending);
 }
 
 /*
@@ -154,6 +167,10 @@ BufferManagerShmemInit(void)
  * compute the size of shared memory for the buffer pool including
  * data pages, buffer descriptors, hash tables, etc.
  *
+ * All the data structures except the buffer blocks are maximally allocated to
+ * accomodate a buffer pool of size MaxNBuffers. Only buffer blocks are
+ * allocated based on the initial size of the buffer pool (NBuffersPending).
+ *
  * The function adds the amount of required memory for buffer blocks to
  * BUFFERS_SHMEM_SEGMENT segment. Amount of memory required for other structures
  * is returned.
@@ -165,13 +182,15 @@ BufferManagerShmemSize(MemoryMappingSizes *mapping_sizes)
 
 	/* size of data pages, plus alignment padding */
 	size = add_size(0, PG_IO_ALIGN_SIZE);
-	size = add_size(size, mul_size(NBuffers, BLCKSZ));
+	size = add_size(size, mul_size(NBuffersPending, BLCKSZ));
 	mapping_sizes[BUFFERS_SHMEM_SEGMENT].shmem_req_size = size;
+	size = add_size(0, PG_IO_ALIGN_SIZE);
+	size = add_size(size, mul_size(MaxNBuffers, BLCKSZ));
 	mapping_sizes[BUFFERS_SHMEM_SEGMENT].shmem_reserved = size;
 
 	size = 0;
 	/* size of buffer descriptors */
-	size = add_size(size, mul_size(NBuffers, sizeof(BufferDescPadded)));
+	size = add_size(size, mul_size(MaxNBuffers, sizeof(BufferDescPadded)));
 	/* to allow aligning buffer descriptors */
 	size = add_size(size, PG_CACHE_LINE_SIZE);
 
@@ -179,13 +198,156 @@ BufferManagerShmemSize(MemoryMappingSizes *mapping_sizes)
 	size = add_size(size, StrategyShmemSize());
 
 	/* size of I/O condition variables */
-	size = add_size(size, mul_size(NBuffers,
+	size = add_size(size, mul_size(MaxNBuffers,
 								   sizeof(ConditionVariableMinimallyPadded)));
 	/* to allow aligning the above */
 	size = add_size(size, PG_CACHE_LINE_SIZE);
 
 	/* size of checkpoint sort array in bufmgr.c */
-	size = add_size(size, mul_size(NBuffers, sizeof(CkptSortItem)));
+	size = add_size(size, mul_size(MaxNBuffers, sizeof(CkptSortItem)));
 
 	return size;
 }
+
+/*
+ * Resize and reinitialize shared buffer manager structures when resizing the buffer pool.
+ *
+ * This function is called in the backend which coordinates buffer resizing
+ * operation.
+ */
+void
+BufferManagerShmemResize(int currentNBuffers, int targetNBuffers)
+{
+	bool		found;
+	int			i;
+	void	   *tmpPtr;
+
+	/*
+	 * The only resizable buffer manager structure is the buffer blocks. All
+	 * other structures are maximally allocated when the server starts.
+	 */
+	tmpPtr = (char *)
+		TYPEALIGN(PG_IO_ALIGN_SIZE,
+				  ShmemResizeStructInSegment("Buffer Blocks",
+											 targetNBuffers * (Size) BLCKSZ + PG_IO_ALIGN_SIZE,
+											 &found, BUFFERS_SHMEM_SEGMENT));
+	if (BufferBlocks != tmpPtr || !found)
+		elog(FATAL, "resizing buffer blocks failed: expected pointer %p, got %p, found=%d",
+			 BufferBlocks, tmpPtr, found);
+
+	/*
+	 * TODO: Initialize the headers for new buffers. If we are shrinking the
+	 * buffers, invalidate the extra buffers. If expanding initialize the new
+	 * buffers.
+	 */
+	for (i = currentNBuffers; i < targetNBuffers; i++)
+		InitializeBuffer(i);
+}
+
+/*
+ * BufferManagerShmemValidate
+ *		Validate that buffer manager shared memory structures have correct
+ *		pointers and sizes after a resize operation.
+ *
+ * This function is called by backends during ProcessBarrierShmemResizeStruct
+ * to ensure their view of the buffer structures is consistent after memory
+ * remapping.
+ */
+void
+BufferManagerShmemValidate(int targetNBuffers)
+{
+	bool		found;
+	void	   *tmpPtr;
+
+	/* Validate Buffer Descriptors */
+	tmpPtr = (BufferDescPadded *)
+		ShmemInitStructInSegment("Buffer Descriptors",
+								 MaxNBuffers * sizeof(BufferDescPadded),
+								 &found, MAIN_SHMEM_SEGMENT);
+	if (!found || BufferDescriptors != tmpPtr)
+		elog(FATAL, "validating buffer descriptors failed: expected pointer %p, got %p, found=%d",
+			 BufferDescriptors, tmpPtr, found);
+
+	/* Validate Buffer IO Condition Variables */
+	tmpPtr = (ConditionVariableMinimallyPadded *)
+		ShmemInitStructInSegment("Buffer IO Condition Variables",
+								 MaxNBuffers * sizeof(ConditionVariableMinimallyPadded),
+								 &found, MAIN_SHMEM_SEGMENT);
+	if (!found || BufferIOCVArray != tmpPtr)
+		elog(FATAL, "validating buffer IO condition variables failed: expected pointer %p, got %p, found=%d",
+			 BufferIOCVArray, tmpPtr, found);
+
+	/* Validate Checkpoint BufferIds */
+	tmpPtr = (CkptSortItem *)
+		ShmemInitStructInSegment("Checkpoint BufferIds",
+								 MaxNBuffers * sizeof(CkptSortItem), &found,
+								 MAIN_SHMEM_SEGMENT);
+	if (!found || CkptBufferIds != tmpPtr)
+		elog(FATAL, "validating checkpoint buffer IDs failed: expected pointer %p, got %p, found=%d",
+			 CkptBufferIds, tmpPtr, found);
+
+	/* Validate Buffer Blocks */
+	tmpPtr = (char *)
+		TYPEALIGN(PG_IO_ALIGN_SIZE,
+				  ShmemInitStructInSegment("Buffer Blocks",
+										   targetNBuffers * (Size) BLCKSZ + PG_IO_ALIGN_SIZE,
+										   &found, BUFFERS_SHMEM_SEGMENT));
+	if (!found || BufferBlocks != tmpPtr)
+		elog(FATAL, "validating buffer blocks failed: expected pointer %p, got %p, found=%d",
+			 BufferBlocks, tmpPtr, found);
+}
+
+/*
+ * check_shared_buffers
+ *		GUC check_hook for shared_buffers
+ *
+ * When reloading the configuration, shared_buffers should not be set to a value
+ * higher than max_shared_buffers fixed at the boot time.
+ */
+bool
+check_shared_buffers(int *newval, void **extra, GucSource source)
+{
+	if (finalMaxNBuffers && *newval > MaxNBuffers)
+	{
+		GUC_check_errdetail("\"shared_buffers\" must be less than \"max_shared_buffers\".");
+		return false;
+	}
+	return true;
+}
+
+/*
+ * show_shared_buffers
+ *		GUC show_hook for shared_buffers
+ *
+ * Shows both current and pending buffer counts with proper unit formatting.
+ */
+const char *
+show_shared_buffers(void)
+{
+	static char buffer[128];
+	int64		current_value,
+				pending_value;
+	const char *current_unit,
+			   *pending_unit;
+	int			currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers);
+
+	if (currentNBuffers == NBuffersPending)
+	{
+		/* No buffer pool resizing pending. */
+		convert_int_from_base_unit(currentNBuffers, GUC_UNIT_BLOCKS, &current_value, &current_unit);
+		snprintf(buffer, sizeof(buffer), INT64_FORMAT "%s", current_value, current_unit);
+	}
+	else
+	{
+		/*
+		 * Shared buffer pool is pending to be resized, show both current and
+		 * pending sizes.
+		 */
+		convert_int_from_base_unit(currentNBuffers, GUC_UNIT_BLOCKS, &current_value, &current_unit);
+		convert_int_from_base_unit(NBuffersPending, GUC_UNIT_BLOCKS, &pending_value, &pending_unit);
+		snprintf(buffer, sizeof(buffer), INT64_FORMAT "%s (pending: " INT64_FORMAT "%s)",
+				 current_value, current_unit, pending_value, pending_unit);
+	}
+
+	return buffer;
+}
diff --git a/src/backend/storage/buffer/buf_resize.c b/src/backend/storage/buffer/buf_resize.c
new file mode 100644
index 00000000000..7b6d46c05ba
--- /dev/null
+++ b/src/backend/storage/buffer/buf_resize.c
@@ -0,0 +1,455 @@
+/*-------------------------------------------------------------------------
+ *
+ * buf_resize.c
+ *	  shared buffer pool resizing functionality
+ *
+ * This module contains the implementation of shared buffer pool resizing,
+ * including the main resize coordination function and barrier processing
+ * functions that synchronize all backends during resize operations.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/storage/buffer/buf_resize.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgwriter.h"
+#include "storage/bufmgr.h"
+#include "storage/buf_internals.h"
+#include "storage/ipc.h"
+#include "storage/pg_shmem.h"
+#include "storage/pmsignal.h"
+#include "storage/procsignal.h"
+#include "storage/shmem.h"
+#include "utils/injection_point.h"
+
+
+/*
+ * Prepare ShmemCtrl for resizing the shared buffer pool.
+ */
+static void
+MarkBufferResizingStart(int targetNBuffers, int currentNBuffers)
+{
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	Assert(pg_atomic_read_u32(&ShmemCtrl->currentNBuffers) == currentNBuffers);
+
+	pg_atomic_write_u32(&ShmemCtrl->targetNBuffers, targetNBuffers);
+	ShmemCtrl->coordinator = MyProcPid;
+}
+
+/*
+ * Reset ShmemCtrl after resizing the shared buffer pool is done.
+ */
+static void
+MarkBufferResizingEnd(int newNBuffers)
+{
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	Assert(pg_atomic_read_u32(&ShmemCtrl->currentNBuffers) == newNBuffers);
+
+	/*
+	 * TODO: should we leave targetNBuffers as is? We are setting it to
+	 * NBuffers in BufferManagerShmemInit().
+	 */
+	pg_atomic_write_u32(&ShmemCtrl->targetNBuffers, 0);
+	ShmemCtrl->coordinator = -1;
+}
+
+/*
+ * Communicate given buffer pool resize barrier to all other backends and the Postmaster.
+ *
+ * ProcSignalBarrier is not sent to the Postmaster but we need the Postmaster to
+ * update its knowledge about the buffer pool so that it can be inherited by the
+ * child processes.
+ */
+static void
+SharedBufferResizeBarrier(ProcSignalBarrierType barrier, const char *barrier_name)
+{
+	WaitForProcSignalBarrier(EmitProcSignalBarrier(barrier));
+	elog(LOG, "all backends acknowledged %s barrier", barrier_name);
+
+#ifdef USE_INJECTION_POINTS
+	/* Injection point specific to this barrier type */
+	switch (barrier)
+	{
+		case PROCSIGNAL_BARRIER_SHBUF_SHRINK:
+			INJECTION_POINT("pgrsb-shrink-barrier-sent", NULL);
+			break;
+		case PROCSIGNAL_BARRIER_SHBUF_RESIZE_MAP_AND_MEM:
+			INJECTION_POINT("pgrsb-resize-barrier-sent", NULL);
+			break;
+		case PROCSIGNAL_BARRIER_SHBUF_EXPAND:
+			INJECTION_POINT("pgrsb-expand-barrier-sent", NULL);
+			break;
+		case PROCSIGNAL_BARRIER_SHBUF_RESIZE_FAILED:
+			/* TODO: Add an injection point here. */
+			break;
+		case PROCSIGNAL_BARRIER_SMGRRELEASE:
+		case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
+
+			/*
+			 * Not relevant in this function but it's here so that the
+			 * compiler can detect any missing shared buffer resizing barrier
+			 * enum here.
+			 */
+			break;
+	}
+#endif							/* USE_INJECTION_POINTS */
+}
+
+/*
+ * C implementation of SQL interface to update the shared buffers according to
+ * the current values of shared_buffers GUCs.
+ *
+ * The current boundaries of the buffer pool are given by two ranges.
+ *
+ * - [1, StrategyControl::activeNBuffers] is the range of buffers from which new
+ * allocations can happen at any time.
+ *
+ * - [1, ShmemCtrl::currentNBuffers] is the range of valid buffers at any given
+ * time.
+ *
+ * Let's assume that before resizing, the number of buffers in the buffer pool is
+ * NBuffersOld. After resizing it is NBuffersNew. Before resizing
+ * StrategyControl::activeNBuffers == ShmemCtrl::currentNBuffers == NBuffersOld.
+ * After the resizing finishes StrategyControl::activeNBuffers ==
+ * ShmemCtrl::currentNBuffers == NBuffersNew. Thus when no resizing happens these
+ * two ranges are same.
+ *
+ * Following steps are performed by the coordinator during resizing.
+ *
+ * 1. Marks resizing in progress to avoid multiple concurrent invocations of this
+ * function.
+ *
+ * 2. When shrinking the shared buffer pool, the coordinator sends SHBUF_SHRINK
+ * ProcSignalBarrier. In response to this barrier background writer is expected
+ * to set StrategyControl::activeNBuffers = NBuffersNew to restrict the new
+ * buffer allocations only to the new buffer pool size and also reset its
+ * internal state. Once every backend has acknowledged the barrier, the
+ * coordinator can be sure that new allocations will not happen in the buffer
+ * pool area being shrunk. Then it evicts the buffers in that area.  Note that
+ * ShmemCtrl::currentNBuffers is still NBuffersOld, since backend may still
+ * access buffers allocated before the resizing started. Buffer eviction may fail
+ * if a buffer being evicted is pinned and the resizing operatino is aborted.
+ * Once the eviction is finished, the extra memory can be freed in the next step.
+ *
+ * 2. This step is executed in both cases, when expanding the buffer pool or
+ * shrinking the buffer pool. The anonymous file backing each of the shared
+ * memory segment containg the buffer pool shared data structures is resized to
+ * the amount of memory required for the new buffer pool size. When expanding the
+ * expanded portion of memory is initialized appropriately.
+ * ShmemCtrl::currentNBuffers is set to NBuffersNew to indicate new range of
+ * valid shared buffers. Every backend is sent SHBUF_RESIZE_MAP_AND_MEM barrier.
+ * All the backends validate that their pointers to the shared buffers structure
+ * are valid and have the right size. Once every backend has acknowledged the
+ * barrier, this step finishes.
+ *
+ * 3. When expanding the buffer pool, the coordinator sends SHBUF_EXPAND barrier
+ * to signal end of expansion. When expadning the background writer, in response
+ * to StrategyControl::activeNBuffers = NBufferNew so that new allocations can
+ * use expanded range of buffer pool.
+ *
+ * TODO: Handle the case when the backend executing this function dies or the
+ * query is cancelled or it hits an error while resizing.
+ */
+Datum
+pg_resize_shared_buffers(PG_FUNCTION_ARGS)
+{
+	bool		result = true;
+	int			currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers);
+	int			targetNBuffers = NBuffersPending;
+	MemoryMappingSizes mapping_sizes[NUM_MEMORY_MAPPINGS];
+
+	if (currentNBuffers == targetNBuffers)
+	{
+		elog(LOG, "shared buffers are already at %d, no need to resize", currentNBuffers);
+		PG_RETURN_BOOL(true);
+	}
+
+	if (!pg_atomic_test_set_flag(&ShmemCtrl->resize_in_progress))
+	{
+		elog(LOG, "shared buffer resizing already in progress");
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * TODO: NBuffersPending may change after it was sampled above, thus
+	 * leading to wrong memory size estimates. Find a way to pass
+	 * targetNBuffers value to BufferManagerShmemSize().
+	 */
+	BufferManagerShmemSize(mapping_sizes);
+	/* Round it off to a multiple of a typical page size */
+	for (int i = 0; i < NUM_MEMORY_MAPPINGS; i++)
+	{
+		/* Structures in main memory segment are never resized. */
+		if (i == MAIN_SHMEM_SEGMENT)
+			continue;
+
+		round_off_mapping_sizes(&mapping_sizes[i]);
+	}
+
+	/*
+	 * TODO: What if the NBuffersPending value seen here is not the desired
+	 * one because somebody did a pg_reload_conf() between the last
+	 * pg_reload_conf() and execution of this function?
+	 */
+	MarkBufferResizingStart(targetNBuffers, currentNBuffers);
+	elog(LOG, "resizing shared buffers from %d to %d", currentNBuffers, targetNBuffers);
+
+	INJECTION_POINT("pg-resize-shared-buffers-flag-set", NULL);
+
+	/* Phase 1: SHBUF_SHRINK - Only for shrinking buffer pool */
+	if (targetNBuffers < currentNBuffers)
+	{
+		/*
+		 * Phase 1: Shrinking - send SHBUF_SHRINK barrier Every backend sets
+		 * activeNBuffers = NewNBuffers to restrict buffer pool allocations to
+		 * the new size
+		 */
+		elog(LOG, "Phase 1: Shrinking buffer pool, restricting allocations to %d buffers", targetNBuffers);
+
+		SharedBufferResizeBarrier(PROCSIGNAL_BARRIER_SHBUF_SHRINK, CppAsString(PROCSIGNAL_BARRIER_SHBUF_SHRINK));
+
+		/* Evict buffers in the area being shrunk */
+		elog(LOG, "evicting buffers %u..%u", targetNBuffers + 1, currentNBuffers);
+		if (!EvictExtraBuffers(targetNBuffers, currentNBuffers))
+		{
+			elog(WARNING, "failed to evict extra buffers during shrinking");
+			SharedBufferResizeBarrier(PROCSIGNAL_BARRIER_SHBUF_RESIZE_FAILED, CppAsString(PROCSIGNAL_BARRIER_SHBUF_RESIZE_FAILED));
+			MarkBufferResizingEnd(currentNBuffers);
+			pg_atomic_clear_flag(&ShmemCtrl->resize_in_progress);
+			PG_RETURN_BOOL(false);
+		}
+
+		/*
+		 * Shrink buffer manager structures before shrinking the shared
+		 * memory.
+		 */
+		BufferManagerShmemResize(currentNBuffers, targetNBuffers);
+
+		/* Update the current NBuffers. */
+		pg_atomic_write_u32(&ShmemCtrl->currentNBuffers, targetNBuffers);
+	}
+
+	/* Phase 2: SHBUF_RESIZE_MAP_AND_MEM - Both expanding and shrinking */
+	elog(LOG, "Phase 2: Remapping shared memory segments and updating structures");
+	for (int i = 0; i < NUM_MEMORY_MAPPINGS; i++)
+	{
+		/* Structures in the main memory segment are never resized. */
+		if (i == MAIN_SHMEM_SEGMENT)
+			continue;
+
+		if (!PGSharedMemoryResize(i, &mapping_sizes[i]))
+		{
+			/*
+			 * This should never fail since address map should already be
+			 * reserved. So the failure should be treated as PANIC.
+			 */
+			elog(PANIC, "failed to resize anonymous shared memory");
+		}
+	}
+
+	INJECTION_POINT("pgrsb-after-shmem-resize", NULL);
+
+	SharedBufferResizeBarrier(PROCSIGNAL_BARRIER_SHBUF_RESIZE_MAP_AND_MEM, CppAsString(PROCSIGNAL_BARRIER_SHBUF_RESIZE_MAP_AND_MEM));
+
+	/* Phase 3: SHBUF_EXPAND - Only for expanding buffer pool */
+	if (targetNBuffers > currentNBuffers)
+	{
+		/* Expand buffer manager structures after expanding the shared memory. */
+		BufferManagerShmemResize(currentNBuffers, targetNBuffers);
+
+		/*
+		 * Phase 3: Expanding - send SHBUF_EXPAND barrier Backends set
+		 * activeNBuffers = NewNBuffers and start allocating buffers from the
+		 * expanded range
+		 */
+		elog(LOG, "Phase 3: Expanding buffer pool, enabling allocations up to %d buffers", targetNBuffers);
+		pg_atomic_write_u32(&ShmemCtrl->currentNBuffers, targetNBuffers);
+
+		SharedBufferResizeBarrier(PROCSIGNAL_BARRIER_SHBUF_EXPAND, CppAsString(PROCSIGNAL_BARRIER_SHBUF_EXPAND));
+	}
+
+	/*
+	 * Reset buffer resize control area.
+	 */
+	MarkBufferResizingEnd(targetNBuffers);
+
+	pg_atomic_clear_flag(&ShmemCtrl->resize_in_progress);
+
+	elog(LOG, "successfully resized shared buffers to %d", targetNBuffers);
+
+	PG_RETURN_BOOL(result);
+}
+
+bool
+ProcessBarrierShmemShrink(void)
+{
+	int			targetNBuffers = pg_atomic_read_u32(&ShmemCtrl->targetNBuffers);
+
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	/*
+	 * Delay adjusting the new active size of buffer pool till this process
+	 * becomes ready to resize buffers.
+	 */
+	if (delay_shmem_resize)
+	{
+		elog(LOG, "Phase 1: Delaying SHBUF_SHRINK barrier - restricting allocations to %d buffers, coordinator is %d",
+			 targetNBuffers, ShmemCtrl->coordinator);
+
+		return false;
+	}
+
+	if (MyBackendType == B_BG_WRITER)
+	{
+		/*
+		 * We have to reset the background writer's buffer allocation
+		 * statistics and the strategy control together so that background
+		 * writer doesn't go out of sync with ClockSweepTick().
+		 *
+		 * TODO: But in case the background writer is not running, nobody
+		 * would reset the strategy control area. So we can't rely on
+		 * background worker to do that. So find a better way.
+		 */
+		BgBufferSyncReset(NBuffers, targetNBuffers);
+		/* Reset strategy control to new size */
+		StrategyReset(targetNBuffers);
+	}
+
+	elog(LOG, "Phase 1: Processing SHBUF_SHRINK barrier - target buffer pool size = %d, coordinator is %d",
+		 targetNBuffers, ShmemCtrl->coordinator);
+
+	return true;
+}
+
+bool
+ProcessBarrierShmemResizeMapAndMem(void)
+{
+	int			targetNBuffers = pg_atomic_read_u32(&ShmemCtrl->targetNBuffers);
+	int			currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers);
+
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	/*
+	 * If buffer pool is being shrunk, we are already working with a smaller
+	 * buffer pool, so shrinking address space and shared structures should
+	 * not be a problem. When expanding, expanding the address space and
+	 * shared structures beyond the current boundaries is not going to be a
+	 * problem since we are not accessing that memory yet. So there is no
+	 * reason to delay processing this barrier.
+	 */
+
+	/*
+	 * Coordinator has already adjusted its address map and also updated sizes
+	 * of the shared buffer structures, no further validation needed.
+	 */
+	if (ShmemCtrl->coordinator == MyProcPid)
+		return true;
+
+	if (targetNBuffers < currentNBuffers)
+	{
+		/*
+		 * When shrinking, shared data structures have been resized at this
+		 * point.  Validate that their pointers to shared buffer structures
+		 * are still valid and have the correct size after resizing.
+		 *
+		 * TODO: Do want to do this only in assert enabled builds?
+		 */
+		BufferManagerShmemValidate(targetNBuffers);
+		elog(LOG, "Backend %d successfully validated structure pointers after resize", MyProcPid);
+	}
+
+	return true;
+}
+
+bool
+ProcessBarrierShmemExpand(void)
+{
+	int			targetNBuffers = pg_atomic_read_u32(&ShmemCtrl->targetNBuffers);
+
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	/*
+	 * Delay adjusting the new active size of buffer pool till this process
+	 * becomes ready to resize buffers.
+	 */
+	if (delay_shmem_resize)
+	{
+		elog(LOG, "Phase 3: delaying SHBUF_EXPAND barrier - enabling allocations up to %d buffers, coordinator is %d",
+			 targetNBuffers, ShmemCtrl->coordinator);
+		return false;
+	}
+
+	if (MyBackendType == B_BG_WRITER)
+	{
+		/*
+		 * We have to reset the background writer's buffer allocation
+		 * statistics and the strategy control together so that background
+		 * writer doesn't go out of sync with ClockSweepTick().
+		 *
+		 * TODO: But in case the background writer is not running, nobody
+		 * would reset the strategy control area. So we can't rely on
+		 * background worker to do that. So find a better way.
+		 */
+		BgBufferSyncReset(NBuffers, targetNBuffers);
+		StrategyReset(targetNBuffers);
+	}
+
+	/*
+	 * Shared data structures must have been resized by now. Validate that
+	 * their pointers to shared buffer structures are still valid and have the
+	 * correct size after resizing.
+	 *
+	 * TODO: Do want to do this only in assert enabled builds?
+	 */
+	BufferManagerShmemValidate(targetNBuffers);
+	elog(LOG, "Backend %d successfully validated structure pointers after resize", MyProcPid);
+
+	elog(LOG, "Phase 3: Processing SHBUF_EXPAND barrier - targetNBuffers = %d, ShmemCtrl->coordinator = %d", targetNBuffers, ShmemCtrl->coordinator);
+
+	return true;
+}
+
+bool
+ProcessBarrierShmemResizeFailed(void)
+{
+	int			currentNBuffers = pg_atomic_read_u32(&ShmemCtrl->currentNBuffers);
+	int			targetNBuffers = pg_atomic_read_u32(&ShmemCtrl->targetNBuffers);
+
+	Assert(!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress));
+
+	if (MyBackendType == B_BG_WRITER)
+	{
+		/*
+		 * We have to reset the background writer's buffer allocation
+		 * statistics and the strategy control together so that background
+		 * writer doesn't go out of sync with ClockSweepTick().
+		 *
+		 * TODO: But in case the background writer is not running, nobody
+		 * would reset the strategy control area. So we can't rely on
+		 * background worker to do that. So find a better way.
+		 */
+		BgBufferSyncReset(NBuffers, currentNBuffers);
+		/* Reset strategy control to new size */
+		StrategyReset(currentNBuffers);
+	}
+
+	elog(LOG, "received proc signal indicating failure to resize shared buffers from %d to %d, restoring to %d, coordinator is %d",
+		 currentNBuffers, targetNBuffers, currentNBuffers, ShmemCtrl->coordinator);
+
+	return true;
+}
+
+/*
+ * TODO: add progress report facility if required.
+ */
diff --git a/src/backend/storage/buffer/buf_table.c b/src/backend/storage/buffer/buf_table.c
index a33786a460b..0dfdffb25c8 100644
--- a/src/backend/storage/buffer/buf_table.c
+++ b/src/backend/storage/buffer/buf_table.c
@@ -41,7 +41,8 @@ static HTAB *SharedBufHash;
 
 /*
  * Estimate space needed for mapping hashtable
- *		size is the desired hash table size (possibly more than NBuffers)
+ *		size is the desired hash table size (possibly more than the size of buffer
+ *  pool)
  */
 Size
 BufTableShmemSize(int size)
@@ -65,6 +66,13 @@ InitBufTable(int size)
 	info.entrysize = sizeof(BufferLookupEnt);
 	info.num_partitions = NUM_BUFFER_PARTITIONS;
 
+	/*
+	 * The shared buffer look up table is set up only once with maximum
+	 * possible entries considering maximum size of the buffer pool. It is not
+	 * resized after that even if the buffer pool is resized. Hence it is
+	 * allocated in the main shared memory segment and not in a resizeable
+	 * shared memory segment.
+	 */
 	SharedBufHash = ShmemInitHash("Shared Buffer Lookup Table",
 								  size, size,
 								  &info,
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 7241477cac0..be4cd69f2b4 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -57,6 +57,7 @@
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
+#include "storage/pg_shmem.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/read_stream.h"
@@ -223,11 +224,11 @@ static BufferDesc *PinCountWaitBuf = NULL;
  * and, if so, in what mode.
  *
  *
- * To avoid - as we used to - requiring an array with NBuffers entries to keep
- * track of local buffers, we use a small sequentially searched array
- * (PrivateRefCountArrayKeys, with the corresponding data stored in
- * PrivateRefCountArray) and an overflow hash table (PrivateRefCountHash) to
- * keep track of backend local pins.
+ * To avoid - as we used to - requiring an array, with as many entries as the
+ * size of buffer pool, to keep track of local buffers, we use a small
+ * sequentially searched array (PrivateRefCountArrayKeys, with the corresponding
+ * data stored in PrivateRefCountArray) and an overflow hash table
+ * (PrivateRefCountHash) to keep track of backend local pins.
  *
  * Until no more than REFCOUNT_ARRAY_ENTRIES buffers are pinned at once, all
  * refcounts are kept track of in the array; after that, new array entries
@@ -3523,7 +3524,7 @@ BufferSync(int flags)
 						set_bits, 0,
 						0);
 
-		/* Check for barrier events in case NBuffers is large. */
+		/* Check for barrier events in case the buffer pool is large. */
 		if (ProcSignalBarrierPending)
 			ProcessProcSignalBarrier();
 	}
@@ -3720,6 +3721,32 @@ BufferSync(int flags)
 	TRACE_POSTGRESQL_BUFFER_SYNC_DONE(NBuffers, num_written, num_to_scan);
 }
 
+/*
+ * Information saved between BgBufferSync() calls so we can determine the
+ * strategy point's advance rate and avoid scanning already-cleaned buffers. The
+ * variables are global instead of static local so that BgBufferSyncReset() can
+ * adjust it when resizing shared buffers.
+ */
+static bool saved_info_valid = false;
+static int	prev_strategy_buf_id;
+static uint32 prev_strategy_passes;
+static int	next_to_clean;
+static uint32 next_passes;
+
+/* Moving averages of allocation rate and clean-buffer density */
+static float smoothed_alloc = 0;
+static float smoothed_density = 10.0;
+
+void
+BgBufferSyncReset(int currentNBuffers, int targetNBuffers)
+{
+	saved_info_valid = false;
+#ifdef BGW_DEBUG
+	elog(DEBUG2, "invalidated background writer status after resizing buffers from %d to %d",
+		 currentNBuffers, targetNBuffers);
+#endif
+}
+
 /*
  * BgBufferSync -- Write out some dirty buffers in the pool.
  *
@@ -3739,20 +3766,6 @@ BgBufferSync(WritebackContext *wb_context)
 	uint32		strategy_passes;
 	uint32		recent_alloc;
 
-	/*
-	 * Information saved between calls so we can determine the strategy
-	 * point's advance rate and avoid scanning already-cleaned buffers.
-	 */
-	static bool saved_info_valid = false;
-	static int	prev_strategy_buf_id;
-	static uint32 prev_strategy_passes;
-	static int	next_to_clean;
-	static uint32 next_passes;
-
-	/* Moving averages of allocation rate and clean-buffer density */
-	static float smoothed_alloc = 0;
-	static float smoothed_density = 10.0;
-
 	/* Potentially these could be tunables, but for now, not */
 	float		smoothing_samples = 16;
 	float		scan_whole_pool_milliseconds = 120000.0;
@@ -3775,6 +3788,25 @@ BgBufferSync(WritebackContext *wb_context)
 	long		new_strategy_delta;
 	uint32		new_recent_alloc;
 
+	/*
+	 * If buffer pool is being shrunk the buffer being written out may not
+	 * remain valid. If the buffer pool is being expanded, more buffers will
+	 * become available without even this function writing out any. Hence wait
+	 * till buffer resizing finishes i.e. go into hibernation mode.
+	 *
+	 * TODO: We may not need this synchronization if background worker itself
+	 * becomes the coordinator.
+	 */
+	if (!pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress))
+		return true;
+
+	/*
+	 * Resizing shared buffers while this function is performing an LRU scan
+	 * on them may lead to wrong results. Indicate that the resizing should
+	 * wait for the LRU scan to complete.
+	 */
+	delay_shmem_resize = true;
+
 	/*
 	 * Find out where the clock-sweep currently is, and how many buffer
 	 * allocations have happened since our last call.
@@ -3792,6 +3824,7 @@ BgBufferSync(WritebackContext *wb_context)
 	if (bgwriter_lru_maxpages <= 0)
 	{
 		saved_info_valid = false;
+		delay_shmem_resize = false;
 		return true;
 	}
 
@@ -3951,8 +3984,17 @@ BgBufferSync(WritebackContext *wb_context)
 	num_written = 0;
 	reusable_buffers = reusable_buffers_est;
 
-	/* Execute the LRU scan */
-	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
+	/*
+	 * Execute the LRU scan.
+	 *
+	 * If buffer pool is being shrunk, the buffer being written may not remain
+	 * valid. If the buffer pool is being expanded, more buffers will become
+	 * available without even this function writing any. Hence stop what we
+	 * are doing. This also unblocks other processes that are waiting for
+	 * buffer resizing to finish.
+	 */
+	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est &&
+		   !pg_atomic_unlocked_test_flag(&ShmemCtrl->resize_in_progress))
 	{
 		int			sync_state = SyncOneBuffer(next_to_clean, true,
 											   wb_context);
@@ -4011,6 +4053,9 @@ BgBufferSync(WritebackContext *wb_context)
 #endif
 	}
 
+	/* Let the resizing commence. */
+	delay_shmem_resize = false;
+
 	/* Return true if OK to hibernate */
 	return (bufs_to_lap == 0 && recent_alloc == 0);
 }
@@ -4341,7 +4386,29 @@ DebugPrintBufferRefcount(Buffer buffer)
 void
 CheckPointBuffers(int flags)
 {
+	/*
+	 * Mark that buffer sync is in progress - delay any shared memory
+	 * resizing.
+	 */
+	/*
+	 * TODO: We need to assess whether we should allow checkpoint and buffer
+	 * resizing to run in parallel. When expanding buffers it may be fine to
+	 * let the checkpointer run in RESIZE_MAP_AND_MEM phase but delay phase
+	 * EXPAND phase till the checkpoint finishes, at the same time not allow
+	 * checkpoint to run during expansion phase. When shrinking the buffers,
+	 * we should delay SHRINK phase till checkpoint finishes and not allow to
+	 * start checkpoint till SHRINK phase is done, but allow it to run in
+	 * RESIZE_MAP_AND_MEM phase. This needs careful analysis and testing.
+	 */
+	delay_shmem_resize = true;
+
 	BufferSync(flags);
+
+	/*
+	 * Mark that buffer sync is no longer in progress - allow shared memory
+	 * resizing
+	 */
+	delay_shmem_resize = false;
 }
 
 /*
@@ -8538,3 +8605,70 @@ const PgAioHandleCallbacks aio_local_buffer_readv_cb = {
 	.complete_local = local_buffer_readv_complete,
 	.report = buffer_readv_report,
 };
+
+/*
+ * When shrinking shared buffers pool, evict the buffers which will not be part
+ * of the shrunk buffer pool.
+ */
+bool
+EvictExtraBuffers(int targetNBuffers, int currentNBuffers)
+{
+	bool		result = true;
+
+	Assert(targetNBuffers < currentNBuffers);
+
+	/*
+	 * If the buffer being evicated is locked, this function will need to
+	 * wait. This function should not be called from a Postmaster since it can
+	 * not wait on a lock.
+	 */
+	Assert(IsUnderPostmaster);
+
+	/*
+	 * TODO: Before evicting any buffer, we should check whether any of the
+	 * buffers are pinned. If we find that a buffer is pinned after evicting
+	 * most of them, that will impact performance since all those evicted
+	 * buffers might need to be read again.
+	 */
+	for (Buffer buf = targetNBuffers + 1; buf <= currentNBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint64		buf_state;
+		bool		buffer_flushed;
+
+		buf_state = pg_atomic_read_u64(&desc->state);
+
+		/*
+		 * Nobody is expected to touch the buffers while resizing is going one
+		 * hence unlocked precheck should be safe and saves some cycles.
+		 */
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		/*
+		 * XXX: Looks like CurrentResourceOwner can be NULL here, find another
+		 * one in that case?
+		 */
+		if (CurrentResourceOwner)
+			ResourceOwnerEnlarge(CurrentResourceOwner);
+
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		/*
+		 * Now that we have locked buffer descriptor, make sure that the
+		 * buffer without valid data has been skipped above.
+		 */
+		Assert(buf_state & BM_VALID);
+
+		if (!EvictUnpinnedBufferInternal(desc, &buffer_flushed))
+		{
+			elog(WARNING, "could not remove buffer %u, it is pinned", buf);
+			result = false;
+			break;
+		}
+	}
+
+	return result;
+}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 403890055be..6221e035024 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -33,10 +33,16 @@ typedef struct
 	/* Spinlock: protects the values below */
 	slock_t		buffer_strategy_lock;
 
+	/*
+	 * Number of active buffers that can be allocated. During buffer resizing,
+	 * this may be different from the actual size of the buffer pool.
+	 */
+	pg_atomic_uint32 activeNBuffers;
+
 	/*
 	 * clock-sweep hand: index of next buffer to consider grabbing. Note that
 	 * this isn't a concrete buffer - we only ever increase the value. So, to
-	 * get an actual buffer, it needs to be used modulo NBuffers.
+	 * get an actual buffer, it needs to be used modulo activeNBuffers.
 	 */
 	pg_atomic_uint32 nextVictimBuffer;
 
@@ -101,21 +107,27 @@ static inline uint32
 ClockSweepTick(void)
 {
 	uint32		victim;
+	int			activeBuffers;
 
 	/*
 	 * Atomically move hand ahead one buffer - if there's several processes
 	 * doing this, this can lead to buffers being returned slightly out of
-	 * apparent order.
+	 * apparent order. We need to read both the current position of hand and
+	 * the current buffer allocation limit together consistently. They may be
+	 * reset by concurrent resize.
 	 */
+	SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
 	victim =
 		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
+	activeBuffers = pg_atomic_read_u32(&StrategyControl->activeNBuffers);
+	SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 
-	if (victim >= NBuffers)
+	if (victim >= activeBuffers)
 	{
 		uint32		originalVictim = victim;
 
 		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
+		victim = victim % activeBuffers;
 
 		/*
 		 * If we're the one that just caused a wraparound, force
@@ -143,7 +155,7 @@ ClockSweepTick(void)
 				 */
 				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
 
-				wrapped = expected % NBuffers;
+				wrapped = expected % activeBuffers;
 
 				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
 														 &expected, wrapped);
@@ -228,7 +240,8 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/* Use the "clock sweep" algorithm to find a free buffer */
-	trycounter = NBuffers;
+	trycounter = pg_atomic_read_u32(&StrategyControl->activeNBuffers);
+
 	for (;;)
 	{
 		uint64		old_buf_state;
@@ -281,7 +294,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
-					trycounter = NBuffers;
+					trycounter = pg_atomic_read_u32(&StrategyControl->activeNBuffers);
 					break;
 				}
 			}
@@ -323,10 +336,12 @@ StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc)
 {
 	uint32		nextVictimBuffer;
 	int			result;
+	uint32		activeNBuffers;
 
 	SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
 	nextVictimBuffer = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
-	result = nextVictimBuffer % NBuffers;
+	activeNBuffers = pg_atomic_read_u32(&StrategyControl->activeNBuffers);
+	result = nextVictimBuffer % activeNBuffers;
 
 	if (complete_passes)
 	{
@@ -336,7 +351,7 @@ StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc)
 		 * Additionally add the number of wraparounds that happened before
 		 * completePasses could be incremented. C.f. ClockSweepTick().
 		 */
-		*complete_passes += nextVictimBuffer / NBuffers;
+		*complete_passes += nextVictimBuffer / activeNBuffers;
 	}
 
 	if (num_buf_alloc)
@@ -383,7 +398,7 @@ StrategyShmemSize(void)
 	Size		size = 0;
 
 	/* size of lookup hash table ... see comment in StrategyInitialize */
-	size = add_size(size, BufTableShmemSize(NBuffers + NUM_BUFFER_PARTITIONS));
+	size = add_size(size, BufTableShmemSize(MaxNBuffers + NUM_BUFFER_PARTITIONS));
 
 	/* size of the shared replacement strategy control block */
 	size = add_size(size, MAXALIGN(sizeof(BufferStrategyControl)));
@@ -391,6 +406,32 @@ StrategyShmemSize(void)
 	return size;
 }
 
+void
+StrategyReset(int activeNBuffers)
+{
+	Assert(StrategyControl);
+
+	SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+
+	/* Update the active buffer count for the strategy */
+	pg_atomic_write_u32(&StrategyControl->activeNBuffers, activeNBuffers);
+
+	/* Reset the clock-sweep pointer to start from beginning */
+	pg_atomic_write_u32(&StrategyControl->nextVictimBuffer, 0);
+
+	/*
+	 * The statistics is viewed in the context of the number of shared
+	 * buffers. Reset it as the size of active number of shared buffers
+	 * changes.
+	 */
+	StrategyControl->completePasses = 0;
+	pg_atomic_write_u32(&StrategyControl->numBufferAllocs, 0);
+
+	/* TODO: Do we need to seset background writer notifications? */
+	StrategyControl->bgwprocno = -1;
+	SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+}
+
 /*
  * StrategyInitialize -- initialize the buffer cache replacement
  *		strategy.
@@ -408,12 +449,21 @@ StrategyInitialize(bool init)
 	 *
 	 * Since we can't tolerate running out of lookup table entries, we must be
 	 * sure to specify an adequate table size here.  The maximum steady-state
-	 * usage is of course NBuffers entries, but BufferAlloc() tries to insert
-	 * a new entry before deleting the old.  In principle this could be
-	 * happening in each partition concurrently, so we could need as many as
-	 * NBuffers + NUM_BUFFER_PARTITIONS entries.
+	 * usage is of course is as many number of entries as the number of
+	 * buffers in the buffer pool.  Right now there is no way to free shared
+	 * memory. Even if we shrink the buffer lookup table when shrinking the
+	 * buffer pool the unused hash table entries can not be freed. When we
+	 * expand the buffer pool, more entries can be allocated but we can not
+	 * resize the hash table directory without rehashing all the entries. Just
+	 * allocating more entries will lead to more contention. Hence we setup
+	 * the buffer lookup table considering the maximum possible size of the
+	 * buffer pool which is MaxNBuffers.
+	 *
+	 * Additionally BufferAlloc() tries to insert a new entry before deleting
+	 * the old.  In principle this could be happening in each partition
+	 * concurrently, so we need extra NUM_BUFFER_PARTITIONS entries.
 	 */
-	InitBufTable(NBuffers + NUM_BUFFER_PARTITIONS);
+	InitBufTable(MaxNBuffers + NUM_BUFFER_PARTITIONS);
 
 	/*
 	 * Get or create the shared strategy control block
@@ -432,6 +482,8 @@ StrategyInitialize(bool init)
 
 		SpinLockInit(&StrategyControl->buffer_strategy_lock);
 
+		/* Initialize the active buffer count */
+		pg_atomic_init_u32(&StrategyControl->activeNBuffers, NBuffersPending);
 		/* Initialize the clock-sweep pointer */
 		pg_atomic_init_u32(&StrategyControl->nextVictimBuffer, 0);
 
@@ -669,12 +721,25 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		strategy->current = 0;
 
 	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
+	 * If the slot hasn't been filled yet or the buffer in the slot has been
+	 * invalidated when buffer pool was shrunk, tell the caller to allocate a
+	 * new buffer with the normal allocation strategy.  He will then fill this
 	 * slot by calling AddBufferToRing with the new buffer.
+	 *
+	 * TODO: buffer ids in the ring will never be greater than the size of
+	 * buffer pool, except maybe the first time ring is accessed after
+	 * shrinking the buffer pool. Checking the upper bound on buffer id always
+	 * may mask a bug bugs that introduces buffer ids higher than the size of
+	 * buffer pool in the ring. But performing that check only once after
+	 * shrinking seems impossible.  The BufferAccessStrategy objects are not
+	 * accessible outside the ScanState. Hence we can not purge the buffers
+	 * while evicting the buffers.  After the resizing is finished, it's not
+	 * possible to notice when we touch the first of those objects and the
+	 * last of objects. See if this can fixed.
 	 */
 	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
+	if (bufnum == InvalidBuffer ||
+		bufnum > pg_atomic_read_u32(&StrategyControl->activeNBuffers))
 		return NULL;
 
 	buf = GetBufferDescriptor(bufnum - 1);
diff --git a/src/backend/storage/buffer/meson.build b/src/backend/storage/buffer/meson.build
index ed84bf08971..f219e29d5ef 100644
--- a/src/backend/storage/buffer/meson.build
+++ b/src/backend/storage/buffer/meson.build
@@ -6,4 +6,5 @@ backend_sources += files(
   'bufmgr.c',
   'freelist.c',
   'localbuf.c',
+  'buf_resize.c',
 )
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 6d2c4520c43..a65436bd7e5 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -156,6 +156,14 @@ CalculateShmemSize(MemoryMappingSizes *mapping_sizes)
 	size = add_size(size, WaitLSNShmemSize());
 	size = add_size(size, LogicalDecodingCtlShmemSize());
 
+	/*
+	 * XXX: For some reason slightly more memory is needed for larger
+	 * shared_buffers, but this size is enough for any large value I've tested
+	 * with. Is it a mistake in how slots are split, or there was a hidden
+	 * inconsistency in shmem calculation?
+	 */
+	size = add_size(size, 1024 * 1024 * 100);
+
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
 
@@ -170,8 +178,7 @@ CalculateShmemSize(MemoryMappingSizes *mapping_sizes)
 	/* might as well round it off to a multiple of a typical page size */
 	for (int segment = 0; segment < NUM_MEMORY_MAPPINGS; segment++)
 	{
-		mapping_sizes[segment].shmem_req_size = add_size(mapping_sizes[segment].shmem_req_size, 8192 - (mapping_sizes[segment].shmem_req_size % 8192));
-		mapping_sizes[segment].shmem_reserved = add_size(mapping_sizes[segment].shmem_reserved, 8192 - (mapping_sizes[segment].shmem_reserved % 8192));
+		round_off_mapping_sizes(&mapping_sizes[segment]);
 		/* Compute the total size of all segments */
 		size = size + mapping_sizes[segment].shmem_req_size;
 	}
@@ -323,6 +330,8 @@ CreateOrAttachShmemStructs(void)
 	CommitTsShmemInit();
 	SUBTRANSShmemInit();
 	MultiXactShmemInit();
+	/* TODO: This should be part of BufferManagerShmemInit() */
+	ShmemControlInit();
 	BufferManagerShmemInit();
 
 	/*
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..df9bb7a0f1a 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -24,9 +24,11 @@
 #include "port/pg_bitutils.h"
 #include "replication/logicalworker.h"
 #include "replication/walsender.h"
+#include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
+#include "storage/pg_shmem.h"
 #include "storage/shmem.h"
 #include "storage/sinval.h"
 #include "storage/smgr.h"
@@ -109,6 +111,10 @@ static bool CheckProcSignal(ProcSignalReason reason);
 static void CleanupProcSignalState(int status, Datum arg);
 static void ResetProcSignalBarrierBits(uint32 flags);
 
+#ifdef DEBUG_SHMEM_RESIZE
+bool		delay_proc_signal_init = false;
+#endif
+
 /*
  * ProcSignalShmemSize
  *		Compute space needed for ProcSignal's shared memory
@@ -170,6 +176,44 @@ ProcSignalInit(const uint8 *cancel_key, int cancel_key_len)
 	uint32		old_pss_pid;
 
 	Assert(cancel_key_len >= 0 && cancel_key_len <= MAX_CANCEL_KEY_LENGTH);
+
+#ifdef DEBUG_SHMEM_RESIZE
+
+	/*
+	 * Introduced for debugging purposes. You can change the variable at
+	 * runtime using gdb, then start new backends with delayed ProcSignal
+	 * initialization. Simple pg_usleep wont work here due to SIGHUP interrupt
+	 * needed for testing. Taken from pg_sleep;
+	 */
+	if (delay_proc_signal_init)
+	{
+#define GetNowFloat()	((float8) GetCurrentTimestamp() / 1000000.0)
+		float8		endtime = GetNowFloat() + 5;
+
+		for (;;)
+		{
+			float8		delay;
+			long		delay_ms;
+
+			CHECK_FOR_INTERRUPTS();
+
+			delay = endtime - GetNowFloat();
+			if (delay >= 600.0)
+				delay_ms = 600000;
+			else if (delay > 0.0)
+				delay_ms = (long) (delay * 1000.0);
+			else
+				break;
+
+			(void) WaitLatch(MyLatch,
+							 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+							 delay_ms,
+							 WAIT_EVENT_PG_SLEEP);
+			ResetLatch(MyLatch);
+		}
+	}
+#endif
+
 	if (MyProcNumber < 0)
 		elog(ERROR, "MyProcNumber not set");
 	if (MyProcNumber >= NumProcSignalSlots)
@@ -579,6 +623,18 @@ ProcessProcSignalBarrier(void)
 					case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
 						processed = ProcessBarrierUpdateXLogLogicalInfo();
 						break;
+					case PROCSIGNAL_BARRIER_SHBUF_SHRINK:
+						processed = ProcessBarrierShmemShrink();
+						break;
+					case PROCSIGNAL_BARRIER_SHBUF_RESIZE_MAP_AND_MEM:
+						processed = ProcessBarrierShmemResizeMapAndMem();
+						break;
+					case PROCSIGNAL_BARRIER_SHBUF_EXPAND:
+						processed = ProcessBarrierShmemExpand();
+						break;
+					case PROCSIGNAL_BARRIER_SHBUF_RESIZE_FAILED:
+						processed = ProcessBarrierShmemResizeFailed();
+						break;
 				}
 
 				/*
diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c
index 5cb59d97871..aa6a44e9755 100644
--- a/src/backend/storage/ipc/shmem.c
+++ b/src/backend/storage/ipc/shmem.c
@@ -71,11 +71,19 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "port/pg_numa.h"
+#include "postmaster/bgwriter.h"
+#include "storage/bufmgr.h"
+#include "storage/buf_internals.h"
+#include "storage/ipc.h"
 #include "storage/lwlock.h"
 #include "storage/pg_shmem.h"
+#include "storage/pmsignal.h"
+#include "storage/procsignal.h"
 #include "storage/shmem.h"
 #include "storage/spin.h"
 #include "utils/builtins.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
 
 /*
  * This is the first data structure stored in the shared memory segment, at
@@ -498,8 +506,7 @@ ShmemInitStructInSegment(const char *name, Size size, bool *foundPtr, int segmen
 	{
 		/*
 		 * Structure is in the shmem index so someone else has allocated it
-		 * already.  The size better be the same as the size we are trying to
-		 * initialize to, or there is a name conflict (or worse).
+		 * already. The size better be the same as the size we are trying to
 		 */
 		if (result->size != size)
 		{
@@ -509,6 +516,7 @@ ShmemInitStructInSegment(const char *name, Size size, bool *foundPtr, int segmen
 							" \"%s\": expected %zu, actual %zu",
 							name, size, result->size)));
 		}
+
 		structPtr = result->location;
 	}
 	else
@@ -543,6 +551,97 @@ ShmemInitStructInSegment(const char *name, Size size, bool *foundPtr, int segmen
 	return structPtr;
 }
 
+/*
+ * ShmemResizeStructInSegment -- Resize the given structure in shared memory.
+ *
+ * This function resizes an existing shared memory structure while preserving
+ * the existing memory location.
+ *
+ * Returns: pointer to the existing structure location, if the resize is
+ * successful, otherwise NULL.
+ */
+void *
+ShmemResizeStructInSegment(const char *name, Size size, bool *foundPtr,
+						   int segment_id)
+{
+	ShmemIndexEnt *result;
+	void	   *structPtr;
+	ShmemSegment *segment;
+	PGShmemHeader *shmhdr;
+	ShmemAllocatorData *ShmemAllocator;
+	Size		allocated_size;
+	Size		newFree;
+
+	Assert(segment_id >= 0 && segment_id < NUM_MEMORY_MAPPINGS);
+	Assert(segment_id != MAIN_SHMEM_SEGMENT);	/* main segment structures not
+												 * resizable */
+	Assert(ShmemIndex);
+	Assert(size > 0);
+	segment = &Segments[segment_id];
+	shmhdr = segment->ShmemSegHdr;
+	ShmemAllocator = segment->ShmemAllocator;
+	Assert(shmhdr != NULL);
+
+	LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE);
+	/* Look up the structure in the shmem index */
+	result = (ShmemIndexEnt *)
+		hash_search(ShmemIndex, name, HASH_FIND, foundPtr);
+
+	Assert(*foundPtr);
+	Assert(result);
+	Assert(result->segment_id == segment_id);
+
+	/* Save the existing structure pointer to be returned. */
+	structPtr = result->location;
+
+	/* Cachealign new size */
+	allocated_size = CACHELINEALIGN(size);
+
+	if (allocated_size == result->allocated_size)
+	{
+		result->size = size;
+		/* No need to resize if the existing allocated size is sufficient */
+		LWLockRelease(ShmemIndexLock);
+		return structPtr;
+	}
+
+	SpinLockAcquire(&ShmemAllocator->shmem_lock);
+
+	/*
+	 * The resizable structures are placed in their own segment after the
+	 * header and the spinlock. Hence the memory location where they end are
+	 * same as the start of free memory in that segment.
+	 */
+	Assert((char *) segment->ShmemBase + ShmemAllocator->free_offset == (char *) result->location + result->allocated_size);
+	newFree = ShmemAllocator->free_offset + (allocated_size - result->allocated_size);
+	if (newFree > shmhdr->totalsize)
+	{
+		structPtr = NULL;
+	}
+	else
+	{
+		ShmemAllocator->free_offset = newFree;
+		result->size = size;
+		result->allocated_size = allocated_size;
+	}
+
+	/*
+	 * End of the structure should still be same as the start of free memory
+	 * in the segment
+	 */
+	Assert((char *) segment->ShmemBase + ShmemAllocator->free_offset == (char *) result->location + result->allocated_size);
+
+	SpinLockRelease(&ShmemAllocator->shmem_lock);
+	LWLockRelease(ShmemIndexLock);
+
+	/* note this assert is okay with structPtr == NULL */
+	Assert(structPtr == (void *) CACHELINEALIGN(structPtr));
+
+	return structPtr;
+}
+
+
+
 /*
  * Add two Size values, checking for overflow
  */
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 02e9aaa6bca..7334bfd15e7 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -63,6 +63,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "storage/bufmgr.h"
 #include "storage/ipc.h"
+#include "storage/pg_shmem.h"
 #include "storage/pmsignal.h"
 #include "storage/proc.h"
 #include "storage/procsignal.h"
@@ -4107,6 +4108,9 @@ PostgresSingleUserMain(int argc, char *argv[],
 	/* Initialize size of fast-path lock cache. */
 	InitializeFastPathLocks();
 
+	/* Initialize MaxNBuffers for buffer pool resizing. */
+	InitializeMaxNBuffers();
+
 	/*
 	 * Give preloaded libraries a chance to request additional shared memory.
 	 */
@@ -4297,6 +4301,13 @@ PostgresMain(const char *dbname, const char *username)
 	 */
 	BeginReportingGUCOptions();
 
+	/*
+	 * TODO: The new backend should fetch the shared buffers status. If the
+	 * resizing is going on, it should bring itself upto speed with it. If
+	 * not, simply fetch the latest pointers are sizes. Is this the right
+	 * place to do that?
+	 */
+
 	/*
 	 * Also set up handler to log session end; we have to wait till now to be
 	 * sure Log_disconnections has its final value.
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..bfe5b9c409a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT	"Waiting for the WAL receiver to exit."
 WAL_RECEIVER_WAIT_START	"Waiting for startup process to send initial data for streaming replication."
 WAL_SUMMARY_READY	"Waiting for a new WAL summary to be generated."
 XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at transaction end."
+PM_BUFFER_RESIZE_WAIT	"Waiting for the postmaster to complete shared buffer pool resize operations."
 
 ABI_compatibility:
 
@@ -366,6 +367,7 @@ SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> s
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
 WaitLSN	"Waiting to read or update shared Wait-for-LSN state."
 LogicalDecodingControl	"Waiting to read or update logical decoding status information."
+ShmemResize	"Waiting to resize shared memory."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 36ad708b360..be9d3909c0f 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -139,7 +139,10 @@ int			max_parallel_maintenance_workers = 2;
  * MaxBackends is computed by PostmasterMain after modules have had a chance to
  * register background workers.
  */
-int			NBuffers = 16384;
+int			NBuffers = 0;
+int			NBuffersPending = 16384;
+bool		finalMaxNBuffers = false;
+int			MaxNBuffers = 0;
 int			MaxConnections = 100;
 int			max_worker_processes = 8;
 int			max_parallel_workers = 8;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3f401faf3de..3869ed5201c 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -595,6 +595,55 @@ InitializeFastPathLocks(void)
 		   pg_nextpower2_32(FastPathLockGroupsPerBackend));
 }
 
+/*
+ * Initialize MaxNBuffers variable with validation.
+ *
+ * This must be called after GUCs have been loaded but before shared memory size
+ * is determined.
+ *
+ * Since MaxNBuffers limits the size of the buffer pool, it must be at least as
+ * much as NBuffersPending. If MaxNBuffers is 0 (default), set it to
+ * NBuffersPending. Otherwise, validate that MaxNBuffers is not less than
+ * NBuffersPending.
+ */
+void
+InitializeMaxNBuffers(void)
+{
+	if (MaxNBuffers == 0)		/* default/boot value */
+	{
+		char		buf[32];
+
+		snprintf(buf, sizeof(buf), "%d", NBuffersPending);
+		SetConfigOption("max_shared_buffers", buf, PGC_POSTMASTER,
+						PGC_S_DYNAMIC_DEFAULT);
+
+		/*
+		 * We prefer to report this value's source as PGC_S_DYNAMIC_DEFAULT.
+		 * However, if the DBA explicitly set max_shared_buffers = 0 in the
+		 * config file, then PGC_S_DYNAMIC_DEFAULT will fail to override that
+		 * and we must force the matter with PGC_S_OVERRIDE.
+		 */
+		if (MaxNBuffers == 0)	/* failed to apply it? */
+			SetConfigOption("max_shared_buffers", buf, PGC_POSTMASTER,
+							PGC_S_OVERRIDE);
+	}
+	else
+	{
+		if (MaxNBuffers < NBuffersPending)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("max_shared_buffers (%d) cannot be less than current shared_buffers (%d)",
+							MaxNBuffers, NBuffersPending),
+					 errhint("Increase max_shared_buffers or decrease shared_buffers.")));
+		}
+	}
+
+	Assert(MaxNBuffers > 0);
+	Assert(!finalMaxNBuffers);
+	finalMaxNBuffers = true;
+}
+
 /*
  * Early initialization of a backend (either standalone or under postmaster).
  * This happens even before InitPostgres.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ae9d5f3fb70..6bc35a1132b 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2599,7 +2599,7 @@ convert_to_base_unit(double value, const char *unit,
  * the value without loss.  For example, if the base unit is GUC_UNIT_KB, 1024
  * is converted to 1 MB, but 1025 is represented as 1025 kB.
  */
-static void
+void
 convert_int_from_base_unit(int64 base_value, int base_unit,
 						   int64 *value, const char **unit)
 {
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index c1f1603cd39..422cdb53336 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2030,6 +2030,15 @@
   max => 'MAX_BACKENDS /* XXX? */',
 },
 
+{ name => "max_shared_buffers", type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM',
+  short_desc => 'Sets the upper limit for the shared_buffers value.',
+  flags => 'GUC_UNIT_BLOCKS',
+  variable => 'MaxNBuffers',
+  boot_val => '0',
+  min => '0',
+  max => 'INT_MAX / 2',
+},
+
 { name => 'max_slot_wal_keep_size', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SENDING',
   short_desc => 'Sets the maximum WAL size that can be reserved by replication slots.',
   long_desc => 'Replication slots will be marked as failed, and segments released for deletion or recycling, if this much space is occupied by WAL on disk. -1 means no maximum.',
@@ -2598,13 +2607,15 @@
 
 # We sometimes multiply the number of shared buffers by two without
 # checking for overflow, so we mustn't allow more than INT_MAX / 2.
-{ name => 'shared_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM',
+{ name => 'shared_buffers', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_MEM',
   short_desc => 'Sets the number of shared memory buffers used by the server.',
   flags => 'GUC_UNIT_BLOCKS',
-  variable => 'NBuffers',
+  variable => 'NBuffersPending',
   boot_val => '16384',
   min => '16',
   max => 'INT_MAX / 2',
+  check_hook => 'check_shared_buffers',
+  show_hook => 'show_shared_buffers',
 },
 
 { name => 'shared_memory_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4b27f2a245e..3390b77921c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12703,4 +12703,10 @@
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
 
+{ oid => '9999', descr => 'resize shared buffers according to the value of GUC `shared_buffers`',
+  proname => 'pg_resize_shared_buffers',
+  provolatile => 'v',
+  prorettype => 'bool',
+  proargtypes => '',
+  prosrc => 'pg_resize_shared_buffers'},
 ]
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index db559b39c4d..df6d3d8f4dd 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -173,7 +173,14 @@ extern PGDLLIMPORT bool ExitOnAnyError;
 extern PGDLLIMPORT char *DataDir;
 extern PGDLLIMPORT int data_directory_mode;
 
+/*
+ * TODO: This is no more a GUC variable and does not track the size of the shared
+ * buffer pool; should be removed.
+ */
 extern PGDLLIMPORT int NBuffers;
+extern PGDLLIMPORT int NBuffersPending;
+extern PGDLLIMPORT bool finalMaxNBuffers;
+extern PGDLLIMPORT int MaxNBuffers;
 extern PGDLLIMPORT int MaxBackends;
 extern PGDLLIMPORT int MaxConnections;
 extern PGDLLIMPORT int max_worker_processes;
@@ -502,6 +509,7 @@ extern PGDLLIMPORT ProcessingMode Mode;
 extern void pg_split_opts(char **argv, int *argcp, const char *optstr);
 extern void InitializeMaxBackends(void);
 extern void InitializeFastPathLocks(void);
+extern void InitializeMaxNBuffers(void);
 extern void InitPostgres(const char *in_dbname, Oid dboid,
 						 const char *username, Oid useroid,
 						 bits32 flags,
diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h
index b21445522b1..a12d6b9082a 100644
--- a/src/include/storage/buf.h
+++ b/src/include/storage/buf.h
@@ -17,7 +17,7 @@
 /*
  * Buffer identifiers.
  *
- * Zero is invalid, positive is the index of a shared buffer (1..NBuffers),
+ * Zero is invalid, positive is the index of a shared buffer (1..{size of shared buffer pool}),
  * negative is the index of a local buffer (-1 .. -NLocBuffer).
  */
 typedef int Buffer;
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index f4e9e703b8b..91e28ce19d2 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -137,8 +137,8 @@ StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2),
 
 /*
  * The maximum allowed value of usage_count represents a tradeoff between
- * accuracy and speed of the clock-sweep buffer management algorithm.  A
- * large value (comparable to NBuffers) would approximate LRU semantics.
+ * accuracy and speed of the clock-sweep buffer management algorithm.  A large
+ * value (comparable to the size of buffer pool) would approximate LRU semantics.
  * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the
  * clock-sweep hand to find a free buffer, so in practice we don't want the
  * value to be very large.
@@ -574,6 +574,7 @@ extern void StrategyNotifyBgWriter(int bgwprocno);
 
 extern Size StrategyShmemSize(void);
 extern void StrategyInitialize(bool init);
+extern void StrategyReset(int activeNBuffers);
 
 /* buf_table.c */
 extern Size BufTableShmemSize(int size);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 93348a34378..649a1a35105 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -21,6 +21,7 @@
 #include "storage/bufpage.h"
 #include "storage/pg_shmem.h"
 #include "storage/relfilelocator.h"
+#include "utils/guc.h"
 #include "utils/relcache.h"
 #include "utils/snapmgr.h"
 
@@ -159,6 +160,7 @@ typedef struct WritebackContext WritebackContext;
 
 /* in globals.c ... this duplicates miscadmin.h */
 extern PGDLLIMPORT int NBuffers;
+extern PGDLLIMPORT int NBuffersPending;
 
 /* in bufmgr.c */
 extern PGDLLIMPORT bool zero_damaged_pages;
@@ -221,6 +223,11 @@ typedef enum BufferLockMode
 	BUFFER_LOCK_EXCLUSIVE,
 } BufferLockMode;
 
+/*
+ * prototypes for functions in buf_init.c
+ */
+extern const char *show_shared_buffers(void);
+extern bool check_shared_buffers(int *newval, void **extra, GucSource source);
 
 /*
  * prototypes for functions in bufmgr.c
@@ -341,6 +348,7 @@ extern bool IsBufferCleanupOK(Buffer buffer);
 extern bool HoldingBufferPinThatDelaysRecovery(void);
 
 extern bool BgBufferSync(WritebackContext *wb_context);
+extern void BgBufferSyncReset(int currentNBuffers, int targetNBuffers);
 
 extern uint32 GetPinLimit(void);
 extern uint32 GetLocalPinLimit(void);
@@ -365,10 +373,13 @@ extern void MarkDirtyRelUnpinnedBuffers(Relation rel,
 extern void MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
 										int32 *buffers_already_dirty,
 										int32 *buffers_skipped);
+extern bool EvictExtraBuffers(int targetNBuffers, int currentNBuffers);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
 extern Size BufferManagerShmemSize(MemoryMappingSizes *mapping_sizes);
+extern void BufferManagerShmemResize(int currentNBuffers, int targetNBuffers);
+extern void BufferManagerShmemValidate(int targetNBuffers);
 
 /* in localbuf.c */
 extern void AtProcExit_LocalBuffers(void);
@@ -417,7 +428,7 @@ extern void FreeAccessStrategy(BufferAccessStrategy strategy);
 static inline bool
 BufferIsValid(Buffer bufnum)
 {
-	Assert(bufnum <= NBuffers);
+	Assert(bufnum <= (Buffer) pg_atomic_read_u32(&ShmemCtrl->currentNBuffers));
 	Assert(bufnum >= -NLocBuffer);
 
 	return bufnum != InvalidBuffer;
@@ -471,4 +482,11 @@ BufferGetPage(Buffer buffer)
 
 #endif							/* FRONTEND */
 
+/* buf_resize.c */
+extern Datum pg_resize_shared_buffers(PG_FUNCTION_ARGS);
+extern bool ProcessBarrierShmemShrink(void);
+extern bool ProcessBarrierShmemResizeMapAndMem(void);
+extern bool ProcessBarrierShmemExpand(void);
+extern bool ProcessBarrierShmemResizeFailed(void);
+
 #endif							/* BUFMGR_H */
diff --git a/src/include/storage/ipc.h b/src/include/storage/ipc.h
index f1d0802d048..23003412c9a 100644
--- a/src/include/storage/ipc.h
+++ b/src/include/storage/ipc.h
@@ -66,6 +66,7 @@ typedef void (*shmem_startup_hook_type) (void);
 /* ipc.c */
 extern PGDLLIMPORT bool proc_exit_inprogress;
 extern PGDLLIMPORT bool shmem_exit_inprogress;
+extern PGDLLIMPORT volatile bool delay_shmem_resize;
 
 pg_noreturn extern void proc_exit(int code);
 extern void shmem_exit(int code);
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index e94ebce95b9..cb454f4c81d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
 PG_LWLOCK(54, WaitLSN)
 PG_LWLOCK(55, LogicalDecodingControl)
+PG_LWLOCK(56, ShmemResize)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/include/storage/pg_shmem.h b/src/include/storage/pg_shmem.h
index eafab1dae52..aec8299d2b3 100644
--- a/src/include/storage/pg_shmem.h
+++ b/src/include/storage/pg_shmem.h
@@ -24,7 +24,12 @@
 #ifndef PG_SHMEM_H
 #define PG_SHMEM_H
 
+#include "port/atomics.h"
+#include "storage/barrier.h"
 #include "storage/dsm_impl.h"
+#include "storage/procsignal.h"
+#include "storage/shmem.h"
+#include "utils/guc.h"
 
 
 typedef struct PGShmemHeader	/* standard header for all Postgres shmem */
@@ -98,11 +103,39 @@ typedef struct MemoryMappingSizes
 
 extern PGDLLIMPORT PGUsedShmemInfo UsedShmemInfo[NUM_MEMORY_MAPPINGS];
 
+/*
+ * ShmemControl is shared between backends and helps to coordinate shared
+ * memory resize.
+ *
+ * TODO: I think we need a lock to protect this structure. If we do so, do we
+ * need to use atomic integers?
+ *
+ * TODO: Merge this structure into StrategyControl?
+ */
+typedef struct
+{
+	pg_atomic_flag resize_in_progress;	/* true if resizing is in progress.
+										 * false otherwise. */
+	pg_atomic_uint32 currentNBuffers;	/* Original NBuffers value before
+										 * resize started */
+	pg_atomic_uint32 targetNBuffers;
+	pid_t		coordinator;
+} ShmemControl;
+
+extern PGDLLIMPORT ShmemControl *ShmemCtrl;
+
+/* The phases for shared memory resizing, used by for ProcSignal barrier. */
+#define SHMEM_RESIZE_REQUESTED			0
+#define SHMEM_RESIZE_START				1
+#define SHMEM_RESIZE_DONE				2
+
 /* GUC variables */
 extern PGDLLIMPORT int shared_memory_type;
 extern PGDLLIMPORT int huge_pages;
 extern PGDLLIMPORT int huge_page_size;
 extern PGDLLIMPORT int huge_pages_status;
+extern PGDLLIMPORT bool finalMaxNBuffers;
+extern PGDLLIMPORT int MaxNBuffers;
 
 /* Possible values for huge_pages and huge_pages_status */
 typedef enum
@@ -134,13 +167,15 @@ extern void PGSharedMemoryReAttach(void);
 extern void PGSharedMemoryNoReAttach(void);
 #endif
 
-extern PGShmemHeader *PGSharedMemoryCreate(int segment_id, MemoryMappingSizes *mapping_sizes,
-										   PGShmemHeader **shim);
-extern bool PGSharedMemoryIsInUse(unsigned long id1, unsigned long id2);
-extern void PGSharedMemoryDetach(void);
-extern void GetHugePageSize(Size *hugepagesize, int *mmap_flags,
-							int *memfd_flags);
-extern void PrepareHugePages(void);
+/*
+ * round off mapping size to a multiple of a typical page size.
+ */
+static inline void
+round_off_mapping_sizes(MemoryMappingSizes *mapping_sizes)
+{
+	mapping_sizes->shmem_req_size = add_size(mapping_sizes->shmem_req_size, 8192 - (mapping_sizes->shmem_req_size % 8192));
+	mapping_sizes->shmem_reserved = add_size(mapping_sizes->shmem_reserved, 8192 - (mapping_sizes->shmem_reserved % 8192));
+}
 
 static inline const char *
 MappingName(int segment_id)
@@ -156,5 +191,18 @@ MappingName(int segment_id)
 	}
 }
 
+extern PGShmemHeader *PGSharedMemoryCreate(int segment_id, MemoryMappingSizes *mapping_sizes,
+										   PGShmemHeader **shim);
+extern bool PGSharedMemoryIsInUse(unsigned long id1, unsigned long id2);
+extern void PGSharedMemoryDetach(void);
+extern void GetHugePageSize(Size *hugepagesize, int *mmap_flags,
+							int *memfd_flags);
+extern bool PGSharedMemoryResize(int segment_id, MemoryMappingSizes *mapping_sizes);
+
+extern void PrepareHugePages(void);
+extern const char *show_shared_buffers(void);
+extern bool check_shared_buffers(int *newval, void **extra, GucSource source);
+extern void ShmemControlInit(void);
+
 
 #endif							/* PG_SHMEM_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..9dc9819a72b 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -56,6 +56,15 @@ typedef enum
 	PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
 	PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO,	/* ask to update
 													 * XLogLogicalInfo */
+	PROCSIGNAL_BARRIER_SHBUF_SHRINK,	/* shrink buffer pool - restrict
+										 * allocations to new size */
+	PROCSIGNAL_BARRIER_SHBUF_RESIZE_MAP_AND_MEM,	/* remap shared memory
+													 * segments and update
+													 * structure pointers */
+	PROCSIGNAL_BARRIER_SHBUF_EXPAND,	/* expand buffer pool - enable
+										 * allocations in new range */
+	PROCSIGNAL_BARRIER_SHBUF_RESIZE_FAILED, /* signal backends that the shared
+											 * buffer resizing failed. */
 } ProcSignalBarrierType;
 
 /*
diff --git a/src/include/storage/shmem.h b/src/include/storage/shmem.h
index 5d58b3b39e6..39da4911bcf 100644
--- a/src/include/storage/shmem.h
+++ b/src/include/storage/shmem.h
@@ -39,11 +39,14 @@ extern HTAB *ShmemInitHash(const char *name, int64 init_size, int64 max_size,
 extern void *ShmemInitStruct(const char *name, Size size, bool *foundPtr);
 extern void *ShmemInitStructInSegment(const char *name, Size size,
 									  bool *foundPtr, int segment_id);
+extern void *ShmemResizeStructInSegment(const char *name, Size size,
+										bool *foundPtr, int segment_id);
 extern Size add_size(Size s1, Size s2);
 extern Size mul_size(Size s1, Size s2);
 
 extern PGDLLIMPORT Size pg_get_shmem_pagesize(void);
 
+
 /* ipci.c */
 extern void RequestAddinShmemSpace(Size size);
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index bf39878c43e..72d8ba9e59e 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -459,6 +459,8 @@ extern config_handle *get_config_handle(const char *name);
 extern void AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt);
 extern char *GetConfigOptionByName(const char *name, const char **varname,
 								   bool missing_ok);
+extern void convert_int_from_base_unit(int64 base_value, int base_unit,
+									   int64 *value, const char **unit);
 
 extern void TransformGUCArray(ArrayType *array, List **names,
 							  List **values);
diff --git a/src/test/Makefile b/src/test/Makefile
index 3eb0a06abb4..7a0d74086c1 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -20,7 +20,8 @@ SUBDIRS = \
 	postmaster \
 	recovery \
 	regress \
-	subscription
+	subscription \
+	buffermgr
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/README b/src/test/README
index afdc7676519..77f11607ff7 100644
--- a/src/test/README
+++ b/src/test/README
@@ -15,6 +15,9 @@ examples/
   Demonstration programs for libpq that double as regression tests via
   "make check"
 
+buffermgr/
+  Tests for resizing buffer pool without restarting the server
+
 isolation/
   Tests for concurrent behavior at the SQL level
 
diff --git a/src/test/buffermgr/Makefile b/src/test/buffermgr/Makefile
new file mode 100644
index 00000000000..eb275027fa6
--- /dev/null
+++ b/src/test/buffermgr/Makefile
@@ -0,0 +1,30 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/buffermgr
+#
+# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/buffermgr/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = contrib/pg_buffercache
+
+REGRESS = buffer_resize
+
+# Custom configuration for buffer manager tests
+TEMP_CONFIG = $(srcdir)/buffermgr_test.conf
+
+subdir = src/test/buffermgr
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/buffermgr/README b/src/test/buffermgr/README
new file mode 100644
index 00000000000..c375ad80989
--- /dev/null
+++ b/src/test/buffermgr/README
@@ -0,0 +1,26 @@
+src/test/buffermgr/README
+
+Regression tests for buffer manager
+===================================
+
+This directory contains a test suite for resizing buffer manager without restarting the server.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/buffermgr/buffermgr_test.conf b/src/test/buffermgr/buffermgr_test.conf
new file mode 100644
index 00000000000..a15f3e442a5
--- /dev/null
+++ b/src/test/buffermgr/buffermgr_test.conf
@@ -0,0 +1,11 @@
+# Configuration for buffer manager regression tests
+
+# Even if max_shared_buffers is set multiple times only the last one is used to
+# as the limit on shared_buffers.
+max_shared_buffers = 128kB
+# Set initial shared_buffers as expected by test
+shared_buffers = 128MB
+# Set a larger value for max_shared_buffers to allow testing resize operations
+max_shared_buffers = 300MB
+# Turn huge pages off, since that affects the size of memory segments
+huge_pages = off
diff --git a/src/test/buffermgr/expected/buffer_resize.out b/src/test/buffermgr/expected/buffer_resize.out
new file mode 100644
index 00000000000..47ffa69f3dd
--- /dev/null
+++ b/src/test/buffermgr/expected/buffer_resize.out
@@ -0,0 +1,312 @@
+-- Test buffer pool resizing and shared memory allocation tracking
+-- This test resizes the buffer pool multiple times and monitors
+-- shared memory allocations related to buffer management
+-- TODO: The test sets shared_buffers values in MBs. Instead it could use values
+-- in kBs so that the test runs on very small machines.
+-- TODO: test the actual memory allocated in the shared memory segments.
+-- Create a view for buffer-related shared memory allocations
+CREATE VIEW buffer_allocations AS
+SELECT name, segment, size, allocated_size
+FROM pg_shmem_allocations
+WHERE name IN ('Buffer Blocks', 'Buffer Descriptors', 'Buffer IO Condition Variables',
+               'Checkpoint BufferIds')
+ORDER BY name;
+-- Note: We exclude the 'main' segment even if it contains the shared buffer
+-- lookup table because it contains other shared structures whose total sizes
+-- may vary as the code changes.
+CREATE VIEW buffer_segments AS
+SELECT name, size, reserved_size
+FROM pg_shmem_segments
+WHERE name <> 'main'
+ORDER BY name;
+-- Enable pg_buffercache for buffer count verification
+CREATE EXTENSION IF NOT EXISTS pg_buffercache;
+-- Test 1: Default shared_buffers
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128MB
+(1 row)
+
+SHOW max_shared_buffers;
+ max_shared_buffers 
+--------------------
+ 300MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              | segment |   size    | allocated_size 
+-------------------------------+---------+-----------+----------------
+ Buffer Blocks                 | buffers | 134221824 |      134221824
+ Buffer Descriptors            | main    |   2457600 |        2457600
+ Buffer IO Condition Variables | main    |    614400 |         614400
+ Checkpoint BufferIds          | main    |    768000 |         768000
+(4 rows)
+
+SELECT * FROM buffer_segments;
+  name   |   size    | reserved_size 
+---------+-----------+---------------
+ buffers | 134225920 |     314580992
+(1 row)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        16384
+(1 row)
+
+-- Calling pg_resize_shared_buffers() without changing shared_buffers should be a no-op.
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              | segment |   size    | allocated_size 
+-------------------------------+---------+-----------+----------------
+ Buffer Blocks                 | buffers | 134221824 |      134221824
+ Buffer Descriptors            | main    |   2457600 |        2457600
+ Buffer IO Condition Variables | main    |    614400 |         614400
+ Checkpoint BufferIds          | main    |    768000 |         768000
+(4 rows)
+
+SELECT * FROM buffer_segments;
+  name   |   size    | reserved_size 
+---------+-----------+---------------
+ buffers | 134225920 |     314580992
+(1 row)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        16384
+(1 row)
+
+-- Test 2: Set to 64MB
+ALTER SYSTEM SET shared_buffers = '64MB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+    shared_buffers     
+-----------------------
+ 128MB (pending: 64MB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 64MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              | segment |   size   | allocated_size 
+-------------------------------+---------+----------+----------------
+ Buffer Blocks                 | buffers | 67112960 |       67112960
+ Buffer Descriptors            | main    |  2457600 |        2457600
+ Buffer IO Condition Variables | main    |   614400 |         614400
+ Checkpoint BufferIds          | main    |   768000 |         768000
+(4 rows)
+
+SELECT * FROM buffer_segments;
+  name   |   size   | reserved_size 
+---------+----------+---------------
+ buffers | 67117056 |     314580992
+(1 row)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+         8192
+(1 row)
+
+-- Test 3: Set to 256MB
+ALTER SYSTEM SET shared_buffers = '256MB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+    shared_buffers     
+-----------------------
+ 64MB (pending: 256MB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 256MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              | segment |   size    | allocated_size 
+-------------------------------+---------+-----------+----------------
+ Buffer Blocks                 | buffers | 268439552 |      268439552
+ Buffer Descriptors            | main    |   2457600 |        2457600
+ Buffer IO Condition Variables | main    |    614400 |         614400
+ Checkpoint BufferIds          | main    |    768000 |         768000
+(4 rows)
+
+SELECT * FROM buffer_segments;
+  name   |   size    | reserved_size 
+---------+-----------+---------------
+ buffers | 268443648 |     314580992
+(1 row)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        32768
+(1 row)
+
+-- Test 4: Set to 100MB (non-power-of-two)
+ALTER SYSTEM SET shared_buffers = '100MB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+     shared_buffers     
+------------------------
+ 256MB (pending: 100MB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 100MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              | segment |   size    | allocated_size 
+-------------------------------+---------+-----------+----------------
+ Buffer Blocks                 | buffers | 104861696 |      104861696
+ Buffer Descriptors            | main    |   2457600 |        2457600
+ Buffer IO Condition Variables | main    |    614400 |         614400
+ Checkpoint BufferIds          | main    |    768000 |         768000
+(4 rows)
+
+SELECT * FROM buffer_segments;
+  name   |   size    | reserved_size 
+---------+-----------+---------------
+ buffers | 104865792 |     314580992
+(1 row)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        12800
+(1 row)
+
+-- Test 5: Set to minimum 128kB
+ALTER SYSTEM SET shared_buffers = '128kB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+     shared_buffers     
+------------------------
+ 100MB (pending: 128kB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128kB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              | segment |  size   | allocated_size 
+-------------------------------+---------+---------+----------------
+ Buffer Blocks                 | buffers |  135168 |         135168
+ Buffer Descriptors            | main    | 2457600 |        2457600
+ Buffer IO Condition Variables | main    |  614400 |         614400
+ Checkpoint BufferIds          | main    |  768000 |         768000
+(4 rows)
+
+SELECT * FROM buffer_segments;
+  name   |  size  | reserved_size 
+---------+--------+---------------
+ buffers | 139264 |     314580992
+(1 row)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+           16
+(1 row)
+
+-- Test 6: Try to set shared_buffers higher than max_shared_buffers (should fail)
+ALTER SYSTEM SET shared_buffers = '400MB';
+ERROR:  invalid value for parameter "shared_buffers": 51200
+DETAIL:  "shared_buffers" must be less than "max_shared_buffers".
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+-- This should show the old value since the configuration was rejected
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128kB
+(1 row)
+
+SHOW max_shared_buffers;
+ max_shared_buffers 
+--------------------
+ 300MB
+(1 row)
+
diff --git a/src/test/buffermgr/meson.build b/src/test/buffermgr/meson.build
new file mode 100644
index 00000000000..c24bff721e6
--- /dev/null
+++ b/src/test/buffermgr/meson.build
@@ -0,0 +1,23 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'buffermgr',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'buffer_resize',
+    ],
+    'regress_args': ['--temp-config', files('buffermgr_test.conf')],
+  },
+  'tap': {
+    'env': {
+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
+    'tests': [
+      't/001_resize_buffer.pl',
+      't/003_parallel_resize_buffer.pl',
+      't/004_client_join_buffer_resize.pl',
+    ],
+  },
+}
diff --git a/src/test/buffermgr/sql/buffer_resize.sql b/src/test/buffermgr/sql/buffer_resize.sql
new file mode 100644
index 00000000000..fc27522f097
--- /dev/null
+++ b/src/test/buffermgr/sql/buffer_resize.sql
@@ -0,0 +1,97 @@
+-- Test buffer pool resizing and shared memory allocation tracking
+-- This test resizes the buffer pool multiple times and monitors
+-- shared memory allocations related to buffer management
+-- TODO: The test sets shared_buffers values in MBs. Instead it could use values
+-- in kBs so that the test runs on very small machines.
+
+-- TODO: test the actual memory allocated in the shared memory segments.
+
+-- Create a view for buffer-related shared memory allocations
+CREATE VIEW buffer_allocations AS
+SELECT name, segment, size, allocated_size
+FROM pg_shmem_allocations
+WHERE name IN ('Buffer Blocks', 'Buffer Descriptors', 'Buffer IO Condition Variables',
+               'Checkpoint BufferIds')
+ORDER BY name;
+
+-- Note: We exclude the 'main' segment even if it contains the shared buffer
+-- lookup table because it contains other shared structures whose total sizes
+-- may vary as the code changes.
+CREATE VIEW buffer_segments AS
+SELECT name, size, reserved_size
+FROM pg_shmem_segments
+WHERE name <> 'main'
+ORDER BY name;
+
+-- Enable pg_buffercache for buffer count verification
+CREATE EXTENSION IF NOT EXISTS pg_buffercache;
+
+-- Test 1: Default shared_buffers
+SHOW shared_buffers;
+SHOW max_shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT * FROM buffer_segments;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+-- Calling pg_resize_shared_buffers() without changing shared_buffers should be a no-op.
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT * FROM buffer_segments;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 2: Set to 64MB
+ALTER SYSTEM SET shared_buffers = '64MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT * FROM buffer_segments;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 3: Set to 256MB
+ALTER SYSTEM SET shared_buffers = '256MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT * FROM buffer_segments;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 4: Set to 100MB (non-power-of-two)
+ALTER SYSTEM SET shared_buffers = '100MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT * FROM buffer_segments;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 5: Set to minimum 128kB
+ALTER SYSTEM SET shared_buffers = '128kB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT * FROM buffer_segments;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 6: Try to set shared_buffers higher than max_shared_buffers (should fail)
+ALTER SYSTEM SET shared_buffers = '400MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+-- This should show the old value since the configuration was rejected
+SHOW shared_buffers;
+SHOW max_shared_buffers;
diff --git a/src/test/buffermgr/t/001_resize_buffer.pl b/src/test/buffermgr/t/001_resize_buffer.pl
new file mode 100644
index 00000000000..30e8bfea9cf
--- /dev/null
+++ b/src/test/buffermgr/t/001_resize_buffer.pl
@@ -0,0 +1,143 @@
+# Copyright (c) 2025-2025, PostgreSQL Global Development Group
+#
+# Minimal test testing shared_buffer resizing under load
+
+use strict;
+use warnings;
+use IPC::Run;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Function to resize buffer pool and verify the change.
+sub apply_and_verify_buffer_change
+{
+	my ($node, $new_size) = @_;
+
+	# Use the new pg_resize_shared_buffers() interface which handles everything synchronously
+	$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$new_size'");
+	$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+
+	# If resize function fails, try a few times before giving up
+	my $max_retries = 5;
+	my $retry_delay = 1; # seconds
+	my $success = 0;
+	for my $attempt (1..$max_retries) {
+		my $result = $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()");
+		if ($result eq 't') {
+			$success = 1;
+			last;
+		}
+
+		# If not the last attempt, wait before retrying
+		if ($attempt < $max_retries) {
+			note "Resizing buffer pool to $new_size, attempt $attempt failed, retrying after $retry_delay seconds...";
+			sleep($retry_delay);
+		}
+	}
+
+	is($success, 1, 'resizing to ' . $new_size . ' succeeded after retries');
+	is($node->safe_psql('postgres', "SHOW shared_buffers"), $new_size,
+		'SHOW after resizing to '. $new_size . ' succeeded');
+}
+
+# Initialize a cluster and start pgbench in the background for concurrent load.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# Permit resizing up to 1GB for this test and let the server start with 128MB.
+$node->append_conf('postgresql.conf', qq{
+max_shared_buffers = 160
+shared_buffers = 16
+log_statement = none
+});
+
+$node->start;
+$node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache");
+my $pgb_scale = 1;
+my $pgb_duration = 120;
+my $pgb_num_clients = 3;
+$node->pgbench(
+	"--initialize --init-steps=dtpvg --scale=$pgb_scale --quiet",
+	0,
+	[qr{^$}],
+	[   # stderr patterns to verify initialization stages
+		qr{dropping old tables},
+		qr{creating tables},
+		qr{done in \d+\.\d\d s }
+	],
+	"pgbench initialization (scale=$pgb_scale)"
+);
+my ($pgbench_stdin, $pgbench_stdout, $pgbench_stderr) = ('', '', '');
+# Use --exit-on-abort so that the test stops on the first server crash or error,
+# thus making it easy to debug the failure. Use -C to increase the chances of a
+# new backend being created while resizing the buffer pool.
+my $pgbench_process = IPC::Run::start(
+	[
+		'pgbench',
+		'-p', $node->port,
+		'-T', $pgb_duration,
+		'-c', $pgb_num_clients,
+		'-C',
+		'--exit-on-abort',
+		'postgres'
+	],
+	'<'  => \$pgbench_stdin,
+	'>'  => \$pgbench_stdout,
+	'2>' => \$pgbench_stderr
+);
+
+ok($pgbench_process, "pgbench started successfully");
+
+# Allow pgbench to establish connections and start generating load.
+#
+# TODO: When creating new backends is known to work well with buffer pool
+# resizing, this wait should be removed.
+sleep(1);
+
+# Resize buffer pool to various sizes while pgbench is running in the
+# background. We use smaller sizes to induce frequent buffer eviction and
+# allocation.  Also smaller buffer pool means frequent wraparound in background
+# writer, default buffer allocation strategy and checkpointer.
+#
+# TODO: These are pseudo-randomly picked sizes, but we can do better.
+my $tests_completed = 0;
+my @buffer_sizes = (32, 24, 29, 40, 29, 20, 16, 24);
+for my $target_size (@buffer_sizes)
+{
+	# Convert number of buffers to a string that will be reported by SHOW
+	# shared_buffers. This simple calculation works for sizes smaller than 128
+	# beyond which the unit changes to MB.
+	$target_size = $target_size * 8;
+	$target_size = $target_size . 'kB';
+
+	# Verify workload generator is still running
+	if (!$pgbench_process->pumpable) {
+		ok(0, "pgbench is still running");
+		last;
+	}
+
+	apply_and_verify_buffer_change($node, $target_size);
+	$tests_completed++;
+
+	# Wait for the resized buffer pool to stabilize. If the resized buffer pool
+	# is utilized fully, it might hit any wrongly initialized areas of shared
+	# memory.
+	sleep(2);
+}
+is($tests_completed, scalar(@buffer_sizes), "All buffer sizes were tested");
+
+# Make sure that pgbench can end normally.
+$pgbench_process->signal('TERM');
+IPC::Run::finish $pgbench_process;
+ok(grep { $pgbench_process->result == $_ } (0, 15),  "pgbench exited gracefully");
+
+# Log any error output from pgbench for debugging
+diag("pgbench stderr:\n$pgbench_stderr");
+diag("pgbench stdout:\n$pgbench_stdout");
+
+# Ensure database is still functional after all the buffer changes
+$node->connect_ok("dbname=postgres",
+	"Database remains accessible after $tests_completed buffer resize operations");
+
+done_testing();
diff --git a/src/test/buffermgr/t/003_parallel_resize_buffer.pl b/src/test/buffermgr/t/003_parallel_resize_buffer.pl
new file mode 100644
index 00000000000..40d4bfde437
--- /dev/null
+++ b/src/test/buffermgr/t/003_parallel_resize_buffer.pl
@@ -0,0 +1,71 @@
+# Copyright (c) 2025-2025, PostgreSQL Global Development Group
+#
+# Test that only one pg_resize_shared_buffers() call succeeds when multiple
+# sessions attempt to resize buffers concurrently
+
+use strict;
+use warnings;
+use IPC::Run;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Skip this test if injection points are not supported
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Initialize a cluster
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points');
+$node->append_conf('postgresql.conf', 'shared_buffers = 128kB');
+$node->append_conf('postgresql.conf', 'max_shared_buffers = 256kB');
+$node->start;
+
+# Load injection points extension for test coordination
+$node->safe_psql('postgres', "CREATE EXTENSION injection_points");
+
+# Test 1: Two concurrent pg_resize_shared_buffers() calls
+# Set up injection point to pause the first resize call
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('pg-resize-shared-buffers-flag-set', 'wait')");
+
+# Change shared_buffers for the resize operation
+$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '144kB'");
+$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+
+# Start first resize session (will pause at injection point)
+my $session1 = $node->background_psql('postgres');
+$session1->query_until(
+	qr/starting_resize/,
+	q(
+		\echo starting_resize
+		SELECT pg_resize_shared_buffers();
+	)
+);
+
+# Wait until session actually reaches the injection point
+$node->wait_for_event('client backend', 'pg-resize-shared-buffers-flag-set');
+
+# Start second resize session (should fail immediately since resize is in progress)
+my $result2 = $node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()");
+
+# The second call should return false (already in progress)
+is($result2, 'f', 'Second concurrent resize call returns false');
+
+# Wake up the first session
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('pg-resize-shared-buffers-flag-set')");
+
+# The pg_resize_shared_buffers() in session1 should now complete successfully
+# We can't easily capture the return value from query_until, but we can
+# verify the session completes without error and the resize actually happened
+$session1->quit;
+
+# Detach injection point
+$node->safe_psql('postgres',
+	"SELECT injection_points_detach('pg-resize-shared-buffers-flag-set')");
+
+done_testing();
diff --git a/src/test/buffermgr/t/004_client_join_buffer_resize.pl b/src/test/buffermgr/t/004_client_join_buffer_resize.pl
new file mode 100644
index 00000000000..072eee535f6
--- /dev/null
+++ b/src/test/buffermgr/t/004_client_join_buffer_resize.pl
@@ -0,0 +1,243 @@
+# Copyright (c) 2025-2025, PostgreSQL Global Development Group
+#
+# Test shared_buffer resizing coordination with client connections joining using injection points
+use strict;
+use warnings;
+use IPC::Run;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+# Skip this test if injection points are not supported
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Function to calculate the size of test table required to fill up maximum
+# buffer pool when populating it.
+sub calculate_test_sizes
+{
+	my ($node, $block_size) = @_;
+
+	# Get the maximum buffer pool size from configuration
+	my $max_shared_buffers = $node->safe_psql('postgres', "SHOW max_shared_buffers");
+	my ($max_val, $max_unit) = ($max_shared_buffers =~ /(\d+)(\w+)/);
+	my $max_size_bytes;
+	if (lc($max_unit) eq 'kb') {
+		$max_size_bytes = $max_val * 1024;
+	} elsif (lc($max_unit) eq 'mb') {
+		$max_size_bytes = $max_val * 1024 * 1024;
+	} elsif (lc($max_unit) eq 'gb') {
+		$max_size_bytes = $max_val * 1024 * 1024 * 1024;
+	} else {
+		# Default to kB if unit is not recognized
+		$max_size_bytes = $max_val * 1024;
+	}
+
+	# Fill more pages than minimally required to increase the chances of pages
+	# from the test table filling the buffer cache.
+	$max_size_bytes = $max_size_bytes;
+	my $pages_needed = int($max_size_bytes / $block_size) + 10; # Add some extra to ensure buffers are filled
+	my $rows_to_insert = $pages_needed * 100; # Assuming roughly 100 rows per page for our table structure
+	return ($max_size_bytes, $pages_needed, $rows_to_insert);
+}
+
+# Function to calculate expected buffer count from size string
+sub calculate_buffer_count
+{
+	my ($size_string, $block_size) = @_;
+	# Parse size and convert to bytes
+	my ($size_val, $unit) = ($size_string =~ /(\d+)(\w+)/);
+	my $size_bytes;
+	if (lc($unit) eq 'kb') {
+		$size_bytes = $size_val * 1024;
+	} elsif (lc($unit) eq 'mb') {
+		$size_bytes = $size_val * 1024 * 1024;
+	} elsif (lc($unit) eq 'gb') {
+		$size_bytes = $size_val * 1024 * 1024 * 1024;
+	} else {
+		# Default to kB if unit is not recognized
+		$size_bytes = $size_val * 1024;
+	}
+	return int($size_bytes / $block_size);
+}
+
+# Initialize cluster with very small buffer sizes for testing
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# Configure for buffer resizing with very small buffer pool sizes for faster tests.
+# TODO: for some reason parallel workers try to load default number of shared_buffers which doesn't work with lower max_shared_buffers. We need to fix that - somewhere it's picking default value of shared buffers. For now disable parallelism
+$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points');
+$node->append_conf('postgresql.conf', qq{
+max_shared_buffers = 512kB
+shared_buffers = 320kB
+max_parallel_workers_per_gather = 0
+});
+$node->start;
+
+# Enable injection points
+$node->safe_psql('postgres', "CREATE EXTENSION injection_points");
+
+# Get the block size (this is fixed for the binary)
+my $block_size = $node->safe_psql('postgres', "SHOW block_size");
+
+# Try to create pg_buffercache extension for buffer analysis
+eval {
+	$node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache");
+};
+if ($@) {
+	$node->stop;
+	plan skip_all => 'pg_buffercache extension not available - cannot verify buffer usage';
+}
+
+# Create a small test table, and fetch its properties for later reference if required.
+$node->safe_psql('postgres', qq{
+	CREATE TABLE client_test (c1 int, data char(50));
+});
+my $table_oid = $node->safe_psql('postgres', "SELECT oid FROM pg_class WHERE relname = 'client_test'");
+my $table_relfilenode = $node->safe_psql('postgres', "SELECT relfilenode FROM pg_class WHERE relname = 'client_test'");
+note("Test table client_test: OID = $table_oid, relfilenode = $table_relfilenode");
+my ($max_size_bytes, $pages_needed, $rows_to_insert) = calculate_test_sizes($node, $block_size);
+
+# Create dedicated sessions for injection point handling and test queries,
+# so that we don't create new backends for test operations after starting
+# resize operation. Only one backend, which tests new backend synchronization
+# with resizing operation, should start after resizing has commenced.
+my $injection_session = $node->background_psql('postgres');
+my $query_session = $node->background_psql('postgres');
+my $resize_session = $node->background_psql('postgres');
+
+# Function to run a single injection point test
+sub run_injection_point_test
+{
+	my ($test_name, $injection_point, $target_size, $operation_type) = @_;
+
+	# Silence the logging of the statements we run to avoid
+	# unnecessarily bloating the test logs.  This runs before the
+	# upgrade we're testing, so the details should not be very
+	# interesting for debugging.  But if needed, you can make it more
+	# verbose by setting this.
+	my $verbose = 0;
+
+	note("Test with $test_name ($operation_type)");
+
+	# Calculate test parameters before starting resize
+	my ($max_size_bytes, $pages_needed, $rows_to_insert) = calculate_test_sizes($node, $target_size, $block_size);
+
+	# Update buffer pool size and wait for it to reflect pending state
+	$resize_session->query_safe("ALTER SYSTEM SET shared_buffers = '$target_size'", verbose => $verbose);
+	$resize_session->query_safe("SELECT pg_reload_conf()", verbose => $verbose);
+	my $pending_size_str = "pending: $target_size";
+	$resize_session->poll_query_until("SELECT substring(current_setting('shared_buffers'), '$pending_size_str')", $pending_size_str, verbose => $verbose);
+
+	# Set up injection point in injection session
+	$injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')", verbose => $verbose);
+
+	# Trigger resize
+	$resize_session->query_until(
+		qr/starting_resize/,
+		q(
+			\echo starting_resize
+			SELECT pg_resize_shared_buffers();
+		)
+	);
+
+	# Wait until resize actually reaches the injection point using the query session
+	$query_session->wait_for_event('client backend', $injection_point, verbose => $verbose);
+
+	# Start a client while resize is paused
+	my $client = $node->background_psql('postgres');
+	note("Background client backend PID: " . $client->query_safe("SELECT pg_backend_pid()", verbose => $verbose));
+
+	# Wake up the injection point from injection session
+	$injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose);
+
+	# Test buffer functionality immediately after waking up injection point
+	# Insert data to test buffer pool functionality during/after resize
+	$client->query_safe("INSERT INTO client_test SELECT i, 'test_data_' || i FROM generate_series(1, $rows_to_insert) i", verbose => $verbose);
+	# Verify the data was inserted correctly and can be read back
+	is($client->query_safe("SELECT COUNT(*) FROM client_test", verbose => $verbose), $rows_to_insert, "inserted $rows_to_insert during $test_name ($operation_type) successful");
+
+	# Verify table size is reasonable (should be substantial for testing)
+	ok($query_session->query_safe("SELECT pg_total_relation_size('client_test')", verbose => $verbose) >=  $max_size_bytes,"table size is large enough to overflow buffer pool in test $test_name ($operation_type)");
+
+	# Wait for the resize operation to complete. There is no direct way to do so
+	# in background_psql. Hence fire a psql command and wait for it to finish
+	$resize_session->query(q(\echo 'done'), verbose => $verbose);
+
+	# Detach injection point from injection session
+	$injection_session->query_safe("SELECT injection_points_detach('$injection_point')", verbose => $verbose);
+
+	# Verify resize completed successfully
+	is($query_session->query_safe("SELECT current_setting('shared_buffers')", verbose => $verbose), $target_size,
+		"resize completed successfully to $target_size");
+
+	# Check buffer pool size using pg_buffercache after resize completion
+	is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", verbose => $verbose), calculate_buffer_count($target_size, $block_size), "all buffers in the buffer pool used in $test_name ($operation_type)");
+
+	# Wait for client to complete
+	ok($client->quit, "client succeeded during $test_name ($operation_type)");
+
+	# Clean up for next test
+	$query_session->query_safe("DELETE FROM client_test", verbose => $verbose);
+}
+
+# Test injection points during buffer resize with client connections
+my @common_injection_tests = (
+	{
+		name => 'flag setting phase',
+		injection_point => 'pg-resize-shared-buffers-flag-set',
+	},
+	{
+		name => 'memory remap phase',
+		injection_point => 'pgrsb-after-shmem-resize',
+	},
+	{
+		name => 'resize map barrier complete',
+		injection_point => 'pgrsb-resize-barrier-sent',
+	},
+);
+
+# Test common injection points for both shrinking and expanding
+foreach my $test (@common_injection_tests)
+{
+	# Test shrinking scenario
+	run_injection_point_test($test->{name}, $test->{injection_point}, '272kB', 'shrinking');
+
+	# Test expanding scenario
+	run_injection_point_test($test->{name}, $test->{injection_point}, '400kB', 'expanding');
+}
+
+my @shrink_only_tests = (
+	{
+		name => 'shrink barrier complete',
+		injection_point => 'pgrsb-shrink-barrier-sent',
+		size => '200kB',
+	}
+);
+foreach my $test (@shrink_only_tests)
+{
+	run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'shrinking only');
+}
+
+my @expand_only_tests = (
+	{
+		name => 'expand barrier complete',
+		injection_point => 'pgrsb-expand-barrier-sent',
+		size => '416kB',
+	}
+);
+
+foreach my $test (@expand_only_tests)
+{
+	run_injection_point_test($test->{name}, $test->{injection_point}, $test->{size}, 'expanding only');
+}
+
+$injection_session->quit;
+$query_session->quit;
+$resize_session->quit;
+
+done_testing();
diff --git a/src/test/meson.build b/src/test/meson.build
index cd45cbf57fb..e9550933063 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -4,6 +4,7 @@ subdir('regress')
 subdir('isolation')
 
 subdir('authentication')
+subdir('buffermgr')
 subdir('postmaster')
 subdir('recovery')
 subdir('subscription')
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index c6ff2dbde4c..079abde29d9 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -61,6 +61,7 @@ use Config;
 use IPC::Run;
 use PostgreSQL::Test::Utils qw(pump_until);
 use Test::More;
+use Time::HiRes                      qw(usleep);
 
 =pod
 
@@ -374,4 +375,79 @@ sub set_query_timer_restart
 	return $self->{query_timer_restart};
 }
 
+=pod
+
+=item $session->poll_query_until($query [, $expected ])
+
+Run B<$query> repeatedly in this background session, until it returns the
+B<$expected> result ('t', or SQL boolean true, by default).
+Continues polling if the query returns an error result.
+Times out after a reasonable number of attempts.
+Returns 1 if successful, 0 if timed out.
+
+=cut
+
+sub poll_query_until
+{
+	my ($self, $query, $expected, %params) = @_;
+
+	$expected = 't' unless defined($expected);    # default value
+
+	my $max_attempts = 10 * $PostgreSQL::Test::Utils::timeout_default;
+	my $attempts = 0;
+	my ($stdout, $stderr_flag);
+
+	while ($attempts < $max_attempts)
+	{
+		($stdout, $stderr_flag) = $self->query($query, %params);
+
+		chomp($stdout);
+
+		# If query succeeded and returned expected result
+		if (!$stderr_flag && $stdout eq $expected)
+		{
+			return 1;
+		}
+
+		# Wait 0.1 second before retrying.
+		usleep(100_000);
+
+		$attempts++;
+	}
+
+	# Give up. Print the output from the last attempt, hopefully that's useful
+	# for debugging.
+	my $stderr_output = $stderr_flag ? $self->{stderr} : '';
+	diag qq(poll_query_until timed out executing this query:
+$query
+expecting this output:
+$expected
+last actual query output:
+$stdout
+with stderr:
+$stderr_output);
+	return 0;
+}
+
+=item $session->wait_for_event(backend_type, wait_event_name)
+
+Poll pg_stat_activity until backend_type reaches wait_event_name using this
+background session.
+
+=cut
+
+sub wait_for_event
+{
+	my ($self, $backend_type, $wait_event_name, %params) = @_;
+
+	$self->poll_query_until(qq[
+		SELECT count(*) > 0 FROM pg_stat_activity
+		WHERE backend_type = '$backend_type' AND wait_event = '$wait_event_name'
+	], undef, %params)
+	  or die
+	  qq(timed out when waiting for $backend_type to reach wait event '$wait_event_name');
+
+	return;
+}
+
 1;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e4dbe1b787b..2a698b1f4e6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2810,6 +2810,7 @@ ShippableCacheEntry
 ShmemAllocatorData
 ShippableCacheKey
 ShmemIndexEnt
+ShmemControl
 ShmemSegment
 ShutdownForeignScan_function
 ShutdownInformation
-- 
2.34.1

