From cee98345803d425d1fa368b949fecb3f1e6faf5d Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Fri, 14 Aug 2026 17:06:19 +0000
Subject: [PATCH v11 2/4] pg_checksums: Refuse interrupted transitions, note
 change is local

An inprogress-on or inprogress-off control file means an online
transition was cut short; running the offline tool on top of it mixes
two procedures.  Refuse it in every mode, with a hint pointing at the
start-stop cycle (or, on a standby, at letting replication finish)
that resets the state.  Also print that the change applies to one
data directory only, with standby-aware wording, since in a
replication setup the same change must be made on every node.

On a primary, inprogress-on cannot survive a graceful stop: the
checksums launcher resolves it from its exit cleanup, and a crashed
primary is already rejected by the existing "cluster must be shut
down" check.  inprogress-off can, however: it is set by the backend
running pg_disable_data_checksums(), and a fast shutdown arriving
between its two barriers leaves it behind in a cleanly shut down
control file, where the next start-stop cycle resolves it at end of
recovery.  A standby can be stopped with either state, since it has
no launcher and only carries forward whatever state the last replayed
WAL record left it in, with a restartpoint persisting that as-is.
The standby is also the deterministic way to reach the new guard,
which is why the test coverage uses one; the primary window would
need an injection point between the two barriers.

The pg_checksums page said the tool still processes all relation files
regardless of an interrupted online transition; describe the refusal
instead.
---
 doc/src/sgml/ref/pg_checksums.sgml            | 11 ++++---
 src/bin/pg_checksums/pg_checksums.c           | 21 +++++++++++++
 .../test_checksums/t/012_offline_standby.pl   | 31 ++++++++++++++-----
 3 files changed, 51 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index 1cb68187e51..cfc6518e740 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -49,11 +49,12 @@ PostgreSQL documentation
   </para>
 
   <para>
-   When enabling checksums with <application>pg_checksums</application>, if
-   checksums were in the process of being enabled using
-   <xref linkend="checksums-online-enable-disable"/>  when the cluster was shut
-   down, <application>pg_checksums</application> will still process all
-   relation files regardless of the progress of online checksum processing.
+   If checksums were in the process of being enabled or disabled using
+   <xref linkend="checksums-online-enable-disable"/> when the cluster was
+   shut down, the control file still records that interrupted state, and
+   <application>pg_checksums</application> refuses to run in any mode.
+   Start the cluster and shut it down cleanly to reset the state, then
+   retry; on a standby, let replication complete the transition first.
   </para>
 
   <para>
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 8c4ae59222d..95757d0424b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -586,6 +586,21 @@ main(int argc, char *argv[])
 		ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
 		pg_fatal("cluster must be shut down");
 
+	/*
+	 * An inprogress state means an online transition was cut short.  A
+	 * standby stopped mid-transition carries either state; a cleanly shut
+	 * down primary can still carry inprogress-off, which a fast shutdown
+	 * during pg_disable_data_checksums() leaves behind, while inprogress-on
+	 * is always resolved by the launcher's exit cleanup.
+	 */
+	if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+		ControlFile->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+	{
+		pg_log_error("an online data checksum state transition was interrupted");
+		pg_log_error_hint("Start and cleanly shut down the cluster once to reset the data checksum state, then retry. On a standby, let replication complete the transition first.");
+		exit(1);
+	}
+
 	if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
 		mode == PG_MODE_CHECK)
 		pg_fatal("data checksums are not enabled in cluster");
@@ -673,6 +688,12 @@ main(int argc, char *argv[])
 			printf(_("Checksums enabled in cluster\n"));
 		else
 			printf(_("Checksums disabled in cluster\n"));
+
+		printf(_("This change applies to this data directory only.\n"));
+		if (ControlFile->state == DB_SHUTDOWNED_IN_RECOVERY)
+			printf(_("This node appears to be a standby; apply the same change to the primary and all other standbys.\n"));
+		else
+			printf(_("In a replication setup, apply the same change to every node while all are stopped, before restarting any of them.\n"));
 	}
 
 	return 0;
diff --git a/src/test/modules/test_checksums/t/012_offline_standby.pl b/src/test/modules/test_checksums/t/012_offline_standby.pl
index 241a1282ae9..5efe11f993e 100644
--- a/src/test/modules/test_checksums/t/012_offline_standby.pl
+++ b/src/test/modules/test_checksums/t/012_offline_standby.pl
@@ -149,7 +149,10 @@ $newnode->stop('immediate');
 
 # Converge the cluster: enable offline on the standby too.
 $standby->stop;
-$standby->checksum_enable_offline;
+command_checks_all(
+	[ 'pg_checksums', '--enable', '-D', $standby->data_dir ],
+	0, [qr/appears to be a standby/],
+	[], 'standby-role notice on offline enable');
 $standby->start;
 test_checksum_state($standby, 'on');
 $primary->wait_for_catchup($standby);
@@ -217,11 +220,11 @@ unlike(
 );
 
 # Scenario 4: a standby stopped while replaying an interrupted online
-# transition keeps the interrupted state in its own control file.  A
-# primary is never caught this way, as its checksums launcher resolves
-# inprogress-on back to off from its exit cleanup.  A standby has no
-# launcher; it carries forward whatever the last replayed record left
-# it in.
+# transition keeps the interrupted state in its own control file, and
+# pg_checksums must refuse to touch it.  A primary is never caught this
+# way, as its checksums launcher resolves inprogress-on back to off from
+# its exit cleanup.  A standby has no launcher; it carries forward
+# whatever the last replayed record left it in.
 
 # Block an online enable on the primary at inprogress-on with a
 # blocking temp table, same trick as in 004_offline.pl.
@@ -235,6 +238,20 @@ wait_for_checksum_state($standby, 'inprogress-on');
 # Stop the standby cleanly; its restartpoint persists inprogress-on to
 # its own control file, since nothing on a standby resolves it away.
 $standby->stop;
+
+command_fails_like(
+	[ 'pg_checksums', '--enable', '-D', $standby->data_dir ],
+	qr/online data checksum state transition was interrupted/,
+	'pg_checksums --enable refuses a standby stopped mid-transition');
+command_fails_like(
+	[ 'pg_checksums', '--check', '-D', $standby->data_dir ],
+	qr/online data checksum state transition was interrupted/,
+	'pg_checksums --check refuses a standby stopped mid-transition');
+command_fails_like(
+	[ 'pg_checksums', '--disable', '-D', $standby->data_dir ],
+	qr/online data checksum state transition was interrupted/,
+	'pg_checksums --disable refuses a standby stopped mid-transition');
+
 $standby->start;
 wait_for_checksum_state($standby, 'inprogress-on');
 
@@ -257,7 +274,7 @@ wait_for_checksum_state($primary, 'on');
 $primary->wait_for_catchup($standby);
 wait_for_checksum_state($standby, 'on');
 
-is( $standby->safe_psql('postgres', "SELECT count(*) FROM t;"),
+is($standby->safe_psql('postgres', "SELECT count(*) FROM t;"),
 	'10001', 'standby readable once the transition completes');
 
 # Scenario 4 continued: the backup taken mid-transition must also
-- 
2.55.0

