From 7a356fe8ec1c54e5976549b1c9e78bb9af81a705 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Mon, 6 Jul 2026 11:31:19 +0200
Subject: [PATCH v2] Allow using spread checkpoints when enabling/disabling
 checksums

This commit allows using spread (throttled) checkpoints while enabling
or disabling data checksums in a running cluster, to limit the impact on
the rest of the system.

While enabling checksums, the process has to rewrite all data pages, and
then perform perform checkpoints at certain points. It was possible to
throttle the rewriting of data pages, to limit the impact on the rest of
the cluster, the ckeckpoints were always issued as CHECKPOINT_FAST. This
can be very disruptive.

When disabling checksums, it's not necessary to rewrite any data pages,
but the checkpoint may still trigger a lot of I/O. So a seemingly cheap
operation (disabling checksums) may get rather disruptive.

This commit introduces a new "fast" parameter for functions initiating
the checksum state change. The default "fast=true" means the requested
checkpoints are fast.
---
 doc/src/sgml/func/func-admin.sgml             | 22 +++++++++++++--
 src/backend/access/transam/xlog.c             | 24 ++++++++++++-----
 src/backend/postmaster/datachecksum_state.c   | 27 +++++++++++++------
 src/include/access/xlog.h                     |  4 +--
 src/include/catalog/pg_proc.dat               | 12 +++++----
 .../test_checksums/t/DataChecksums/Utils.pm   | 24 ++++++++++++++---
 6 files changed, 87 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..97b768638c5 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3158,7 +3158,7 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
         <indexterm>
          <primary>pg_enable_data_checksums</primary>
         </indexterm>
-        <function>pg_enable_data_checksums</function> ( <optional><parameter>cost_delay</parameter> <type>int</type>, <parameter>cost_limit</parameter> <type>int</type></optional> )
+        <function>pg_enable_data_checksums</function> ( <optional><parameter>cost_delay</parameter> <type>int</type> <literal>DEFAULT</literal> <literal>0</literal>, <parameter>cost_limit</parameter> <type>int</type> <literal>DEFAULT</literal> <literal>100</literal>, <parameter>fast</parameter> <type>bool</type> <literal>DEFAULT</literal> <literal>true</literal></optional> )
         <returnvalue>void</returnvalue>
        </para>
        <para>
@@ -3175,6 +3175,15 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
         specified, the process is throttled using the same principles as
         <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
        </para>
+       <para>
+        The <parameter>fast</parameter> specifies whether the checkpoints performed
+        are fast or spread.
+        When set to <literal>true</literal> the checkpoints are fast, which may cause
+        a spike in I/O. When set to <literal>false</literal>, the checkpoints are
+        spread (throttled), which may increase the time needed to enable checksums,
+        but it limits the impact on the system.
+        The default value is <literal>true</literal>.
+       </para>
        </entry>
       </row>
 
@@ -3183,7 +3192,7 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
         <indexterm>
          <primary>pg_disable_data_checksums</primary>
         </indexterm>
-        <function>pg_disable_data_checksums</function> ()
+        <function>pg_disable_data_checksums</function> ( <optional><parameter>fast</parameter> <type>bool</type> <literal>DEFAULT</literal> <literal>false</literal></optional> )
         <returnvalue>void</returnvalue>
        </para>
        <para>
@@ -3193,6 +3202,15 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
         stopped validating data checksums, the data checksum state will be
         set to <literal>off</literal>.
        </para>
+       <para>
+        The <parameter>fast</parameter> specifies whether the checkpoints performed
+        are fast or spread.
+        When set to <literal>true</literal> the checkpoints are fast, which may cause
+        a spike in I/O. When set to <literal>false</literal>, the checkpoints are
+        spread (throttled), which may increase the time needed to disable checksums,
+        but it limits the impact on the system.
+        The default value is <literal>true</literal>.
+       </para>
        </entry>
       </row>
      </tbody>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a81912b7441..aaca0437a5f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4799,9 +4799,10 @@ SetDataChecksumsOnInProgress(void)
  * state transition.
  */
 void
-SetDataChecksumsOn(void)
+SetDataChecksumsOn(bool fast)
 {
 	uint64		barrier;
+	int			flags;
 
 	SpinLockAcquire(&XLogCtl->info_lck);
 
@@ -4815,7 +4816,7 @@ SetDataChecksumsOn(void)
 		SpinLockRelease(&XLogCtl->info_lck);
 		elog(WARNING,
 			 "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
-		SetDataChecksumsOff();
+		SetDataChecksumsOff(fast);
 		return;
 	}
 
@@ -4845,7 +4846,12 @@ SetDataChecksumsOn(void)
 	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
 	END_CRIT_SECTION();
 
-	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+	/* determine flags for the checkpoint */
+	flags = CHECKPOINT_FORCE | CHECKPOINT_WAIT;
+	if (fast)
+		flags |= CHECKPOINT_FAST;
+
+	RequestCheckpoint(flags);
 	WaitForProcSignalBarrier(barrier);
 }
 
@@ -4863,9 +4869,15 @@ SetDataChecksumsOn(void)
  * state transition.
  */
 void
-SetDataChecksumsOff(void)
+SetDataChecksumsOff(bool fast)
 {
 	uint64		barrier;
+	int			flags;
+
+	/* determine flags for the checkpoint(s) */
+	flags = CHECKPOINT_FORCE | CHECKPOINT_WAIT;
+	if (fast)
+		flags |= CHECKPOINT_FAST;
 
 	SpinLockAcquire(&XLogCtl->info_lck);
 
@@ -4907,7 +4919,7 @@ SetDataChecksumsOff(void)
 		MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
 		END_CRIT_SECTION();
 
-		RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+		RequestCheckpoint(flags);
 		WaitForProcSignalBarrier(barrier);
 
 		/*
@@ -4945,7 +4957,7 @@ SetDataChecksumsOff(void)
 	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
 	END_CRIT_SECTION();
 
-	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+	RequestCheckpoint(flags);
 	WaitForProcSignalBarrier(barrier);
 }
 
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 68557c16cb9..7519d44571e 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -307,6 +307,7 @@ typedef struct DataChecksumsStateStruct
 	DataChecksumsWorkerOperation launch_operation;
 	int			launch_cost_delay;
 	int			launch_cost_limit;
+	bool		launch_fast_checkpoint;
 
 	/*
 	 * Is a launcher process currently running?  This is set by the main
@@ -340,6 +341,7 @@ typedef struct DataChecksumsStateStruct
 	DataChecksumsWorkerOperation operation;
 	int			cost_delay;
 	int			cost_limit;
+	bool		fast_checkpoint;
 
 	/*
 	 * Signaling between the launcher and the worker process. Protected by
@@ -382,7 +384,8 @@ static DataChecksumsWorkerOperation operation;
 /* Prototypes */
 static void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
 											 int cost_delay,
-											 int cost_limit);
+											 int cost_limit,
+											 bool fast);
 static void DataChecksumsShmemRequest(void *arg);
 static bool DatabaseExists(Oid dboid);
 static List *BuildDatabaseList(void);
@@ -543,6 +546,8 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
 Datum
 disable_data_checksums(PG_FUNCTION_ARGS)
 {
+	bool		fast = PG_GETARG_BOOL(0);
+
 	PreventCommandDuringRecovery("pg_disable_data_checksums()");
 
 	if (!superuser())
@@ -550,7 +555,7 @@ disable_data_checksums(PG_FUNCTION_ARGS)
 				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				errmsg("must be superuser to change data checksum state"));
 
-	StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+	StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0, fast);
 	PG_RETURN_VOID();
 }
 
@@ -564,6 +569,7 @@ enable_data_checksums(PG_FUNCTION_ARGS)
 {
 	int			cost_delay = PG_GETARG_INT32(0);
 	int			cost_limit = PG_GETARG_INT32(1);
+	bool		fast = PG_GETARG_BOOL(2);
 
 	PreventCommandDuringRecovery("pg_enable_data_checksums()");
 
@@ -582,7 +588,7 @@ enable_data_checksums(PG_FUNCTION_ARGS)
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("cost limit must be greater than zero"));
 
-	StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+	StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit, fast);
 
 	PG_RETURN_VOID();
 }
@@ -602,7 +608,8 @@ enable_data_checksums(PG_FUNCTION_ARGS)
 static void
 StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
 								 int cost_delay,
-								 int cost_limit)
+								 int cost_limit,
+								 bool fast)
 {
 	BackgroundWorker bgw;
 	BackgroundWorkerHandle *bgw_handle;
@@ -622,6 +629,7 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
 	DataChecksumState->launch_operation = op;
 	DataChecksumState->launch_cost_delay = cost_delay;
 	DataChecksumState->launch_cost_limit = cost_limit;
+	DataChecksumState->launch_fast_checkpoint = fast;
 
 	/* Is the launcher already running? If so, what is it doing? */
 	running = DataChecksumState->launcher_running;
@@ -989,7 +997,7 @@ launcher_exit(int code, Datum arg)
 	 * the state to off since processing cannot be resumed.
 	 */
 	if (DataChecksumsInProgressOn())
-		SetDataChecksumsOff();
+		SetDataChecksumsOff(DataChecksumState->fast_checkpoint);
 
 	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 	launcher_running = false;
@@ -1130,6 +1138,7 @@ DataChecksumsWorkerLauncherMain(Datum arg)
 	DataChecksumState->operation = operation;
 	DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 	DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+	DataChecksumState->fast_checkpoint = DataChecksumState->launch_fast_checkpoint;
 	LWLockRelease(DataChecksumsWorkerLock);
 
 	/*
@@ -1184,7 +1193,7 @@ again:
 		 * Data checksums have been set on all pages, set the state to on in
 		 * order to instruct backends to validate checksums on reading.
 		 */
-		SetDataChecksumsOn();
+		SetDataChecksumsOn(DataChecksumState->fast_checkpoint);
 
 		ereport(LOG,
 				errmsg("data checksums are now enabled"));
@@ -1196,7 +1205,7 @@ again:
 
 		pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
 									 PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
-		SetDataChecksumsOff();
+		SetDataChecksumsOff(DataChecksumState->fast_checkpoint);
 		ereport(LOG,
 				errmsg("data checksums are now disabled"));
 	}
@@ -1224,6 +1233,7 @@ done:
 		operation = DataChecksumState->launch_operation;
 		DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 		DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+		DataChecksumState->fast_checkpoint = DataChecksumState->launch_fast_checkpoint;
 		LWLockRelease(DataChecksumsWorkerLock);
 		goto again;
 	}
@@ -1313,7 +1323,7 @@ ProcessAllDatabases(void)
 			 * Disable checksums on cluster, because we failed one of the
 			 * databases and this is an all or nothing process.
 			 */
-			SetDataChecksumsOff();
+			SetDataChecksumsOff(DataChecksumState->fast_checkpoint);
 			ereport(ERROR,
 					errcode(ERRCODE_INSUFFICIENT_RESOURCES),
 					errmsg("data checksums failed to get enabled in all databases, aborting"),
@@ -1676,6 +1686,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 			DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
 			DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+			DataChecksumState->fast_checkpoint = DataChecksumState->launch_fast_checkpoint;
 		}
 		else
 			costs_updated = false;
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 4dd98624204..8a9e8961a6a 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -253,8 +253,8 @@ extern bool DataChecksumsOn(void);
 extern bool DataChecksumsOff(void);
 extern bool DataChecksumsInProgressOn(void);
 extern void SetDataChecksumsOnInProgress(void);
-extern void SetDataChecksumsOn(void);
-extern void SetDataChecksumsOff(void);
+extern void SetDataChecksumsOn(bool fast);
+extern void SetDataChecksumsOff(bool fast);
 extern const char *show_data_checksums(void);
 extern const char *get_checksum_state_string(uint32 state);
 extern void InitLocalDataChecksumState(void);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..c7ef5a9c930 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12415,13 +12415,15 @@
 # data checksum management functions
 { oid => '6507', descr => 'disable data checksums',
   proname => 'pg_disable_data_checksums', provolatile => 'v',
-  proparallel => 'r', prorettype => 'void', proargtypes => '',
-  prosrc => 'disable_data_checksums', proacl => '{POSTGRES=X}' },
+  proparallel => 'r', prorettype => 'void', proargtypes => 'bool',
+  proallargtypes => '{bool}', proargmodes => '{i}', proargnames => '{fast}',
+  proargdefaults => '{true}', prosrc => 'disable_data_checksums',
+  proacl => '{POSTGRES=X}' },
 { oid => '6506', descr => 'enable data checksums',
   proname => 'pg_enable_data_checksums', provolatile => 'v', proparallel => 'r',
-  prorettype => 'void', proargtypes => 'int4 int4',
-  proallargtypes => '{int4,int4}', proargmodes => '{i,i}',
-  proargnames => '{cost_delay,cost_limit}', proargdefaults => '{0,100}',
+  prorettype => 'void', proargtypes => 'int4 int4 bool',
+  proallargtypes => '{int4,int4,bool}', proargmodes => '{i,i,i}',
+  proargnames => '{cost_delay,cost_limit,fast}', proargdefaults => '{0,100,true}',
   prosrc => 'enable_data_checksums', proacl => '{POSTGRES=X}' },
 
 # collation management functions
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
index 3aa533d777f..77c208dd5fd 100644
--- a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -118,6 +118,11 @@ If defined, the function will wait for the state defined in this parameter,
 waiting timing out, before returning.  The function will wait for
 $PostgreSQL::Test::Utils::timeout_default seconds before timing out.
 
+=item fast
+
+The B<fast> determines if checkpoints will be fast or spread, default is
+'true' (which means fast checkpoints).
+
 =back
 
 =cut
@@ -130,13 +135,14 @@ sub enable_data_checksums
 	# Set sane defaults for the parameters
 	$params{cost_delay} = 0 unless (defined($params{cost_delay}));
 	$params{cost_limit} = 100 unless (defined($params{cost_limit}));
+	$params{fast} = 'true' unless (defined($params{fast}));
 
 	my $query = <<'EOQ';
-SELECT pg_enable_data_checksums(%s, %s);
+SELECT pg_enable_data_checksums(%s, %s, %s);
 EOQ
 
 	$postgresnode->safe_psql('postgres',
-		sprintf($query, $params{cost_delay}, $params{cost_limit}));
+		sprintf($query, $params{cost_delay}, $params{cost_limit}, $params{fast}));
 
 	if (defined($params{wait}))
 	{
@@ -166,6 +172,11 @@ waiting timing out, before returning.  The function will wait for
 $PostgreSQL::Test::Utils::timeout_default seconds before timing out.
 Unlike in C<enable_data_checksums> the value of the parameter is discarded.
 
+=item fast
+
+The B<fast> determines if checkpoints will be fast or spread, default is
+'true' (which means fast checkpoints).
+
 =back
 
 =cut
@@ -175,8 +186,15 @@ sub disable_data_checksums
 	my $postgresnode = shift;
 	my %params = @_;
 
+	# Set sane defaults for the parameters
+	$params{fast} = 'true' unless (defined($params{fast}));
+
+	my $query = <<'EOQ';
+SELECT pg_disable_data_checksums(%s);
+EOQ
+
 	$postgresnode->safe_psql('postgres',
-		'SELECT pg_disable_data_checksums();');
+		sprintf($query, $params{fast}));
 
 	if (defined($params{wait}))
 	{
-- 
2.54.0

