public inbox for [email protected]
help / color / mirror / Atom feed[PATCH 08/18] *an exclusive
60+ messages / 10 participants
[nested] [flat]
* [PATCH 08/18] *an exclusive
@ 2021-02-06 21:13 Justin Pryzby <[email protected]>
0 siblings, 0 replies; 60+ messages in thread
From: Justin Pryzby @ 2021-02-06 21:13 UTC (permalink / raw)
3c84046490bed3c22e0873dc6ba492e02b8b9051
---
doc/src/sgml/ref/drop_index.sgml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/src/sgml/ref/drop_index.sgml b/doc/src/sgml/ref/drop_index.sgml
index 85cf23bca2..b6d2c2014f 100644
--- a/doc/src/sgml/ref/drop_index.sgml
+++ b/doc/src/sgml/ref/drop_index.sgml
@@ -45,7 +45,7 @@ DROP INDEX [ CONCURRENTLY ] [ IF EXISTS ] <replaceable class="parameter">name</r
<para>
Drop the index without locking out concurrent selects, inserts, updates,
and deletes on the index's table. A normal <command>DROP INDEX</command>
- acquires exclusive lock on the table, blocking other accesses until the
+ acquires an exclusive lock on the table, blocking other accesses until the
index drop can be completed. With this option, the command instead
waits until conflicting transactions have completed.
</para>
--
2.17.0
--lc9FT7cWel8HagAv
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment; filename="0009-Doc-review-for-psql-dX.patch"
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-02-05 23:15 Tomas Vondra <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-02-05 23:15 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Andres Freund <[email protected]>; +Cc: Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
On 2/5/26 21:01, Daniel Gustafsson wrote:
>> On 3 Feb 2026, at 17:58, Daniel Gustafsson <[email protected]> wrote:
>
>> I've implemented this in the attached patch and it seems very promising.
>
> Turns out I was a bit quick on the send button, and having been focused on the
> failing test_checksums suite I missed running the other suites. (I also
> managed to off-by-one-year name it 202503..) Thanks to some quick off-list
> feedback, here is an updated version which fixes the failing tests and has
> quite a number of smaller touchups.
>
> Like the previous version, I am not sure I have the CHECKPOINT_xxx logic right
> and would love some eyes on it.
>
Thanks for the updated patch. Indeed, I see occasional failures in the
006 test (I suppose 007 would fail too, but I haven't tried).
It's been ages since I looked at this patch, and I managed to forget
most of it. And I'm still learning how this new approach works, so I
don't have a great idea what's the root cause yet. But let me describe
what I see and ask some questions. Maybe that'll help us to figure out
what might be wrong.
Here's what the failing run did:
[22:11:00.610](0.364s) ok 2 - ensure checksums are set to off
[22:11:00.616](0.006s) # LSN before enabling: 0/4F0BCD78
[22:11:00.629](0.013s) ok 3 - ensure data checksums are transitioned to
inprogress-on
[22:11:09.594](8.965s) ok 4 - ensure data checksums are transitioned to on
[22:11:09.600](0.005s) # LSN after enabling: 0/B1CBD290
[22:11:09.675](0.075s) ok 5 - ensure data pages can be read back on primary
### Stopping node "main" using mode fast
# Running: pg_ctl --pgdata
/home/user/work/postgres/src/test/modules/test_checksums/tmp_check/t_006_pgbench_single_main_data/pgdata
--mode fast stop
waiting for server to shut down.... done
Latest checkpoint location: 0/B28CA0D8
Latest checkpoint's REDO location: 0/B28CA0D8
[22:11:11.517](0.486s) ok 8 - ensure checksums are set to on
[22:11:11.525](0.008s) # LSN before disabling: 0/B3B23C88
[22:11:11.539](0.013s) ok 9 - ensure data checksums are transitioned to off
[22:11:11.545](0.006s) # LSN after disabling: 0/B3B3F430
[22:11:13.596](2.052s) ok 10 - ensure data pages can be read back on primary
### Stopping node "main" using mode immediate
# Running: pg_ctl --pgdata
/home/user/work/postgres/src/test/modules/test_checksums/tmp_check/t_006_pgbench_single_main_data/pgdata
--mode immediate stop
waiting for server to shut down.... done
Latest checkpoint location: 0/B28CA158
Latest checkpoint's REDO location: 0/B28CA158
### Starting node "main"
### Stopping node "main" using mode fast
[22:11:14.130](0.428s) not ok 12 - no checksum validation errors in
primary log (during WAL recovery)
[22:11:14.130](0.000s)
[22:11:14.130](0.000s) # Failed test 'no checksum validation errors in
primary log (during WAL recovery)'
# at t/006_pgbench_single.pl line 180.
So it:
1) starts with checksums=off
2) flips them on by LSN 0/B1CBD290
3) restarts the instance with 'fast' mode
4) flips checksums to 'off'
5) restarts the instance with 'immediate' mode (=> crash)
6) finds checksum failures in the server log (after redo)
The failures like this (all seem to be from VM, and the VM page LSNs are
somewhat interesting too - I'll get to that shortly):
2026-02-05 22:11:13.724 CET startup[286330] LOG: redo starts at
0/B28CA1D8
2026-02-05 22:11:13.729 CET startup[286330] LOG: page verification
failed, calculated checksum 17285 but expected 50010 (page LSN 0/A1CF17F0)
2026-02-05 22:11:13.729 CET startup[286330] CONTEXT: WAL redo at
0/B295E930 for Heap/LOCK: ...
2026-02-05 22:11:13.729 CET startup[286330] LOG: invalid page in
block 4 of relation "base/5/16409_vm"; zeroing out page
2026-02-05 22:11:13.732 CET startup[286330] LOG: page verification
failed, calculated checksum 49687 but expected 51187 (page LSN 0/B29F5A20)
2026-02-05 22:11:13.732 CET startup[286330] CONTEXT: WAL redo at
0/B299E5F8 for Heap/UPDATE: ...
2026-02-05 22:11:13.732 CET startup[286330] LOG: invalid page in
block 42 of relation "base/5/16409_fsm"; zeroing out page
...
During the test, the cluster went through these checkpoints / checksums:
lsn: 0/4E00E600, desc: CHECKPOINT_REDO wal_level replica; checksums off
lsn: 0/4F0C8B10, prev 0/4F0C6DD8, desc: CHECKSUMS inprogress-on
lsn: 0/52010FA8, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
lsn: 0/56015400, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
...
lsn: 0/AA005E00, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
lsn: 0/AE004988, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
lsn: 0/B1C7DFC8, prev 0/B1C7DFA0, desc: CHECKSUMS on
lsn: 0/B200BE38, desc: CHECKPOINT_REDO wal_level replica; checksums on
lsn: 0/B28CA0D8, desc: CHECKPOINT_SHUTDOWN redo 0/B28CA0D8; ...
checksums on; shutdown
lsn: 0/B28CA158, desc: CHECKPOINT_SHUTDOWN redo 0/B28CA158; ...
checksums on; shutdown
lsn: 0/B3B2F780, prev 0/B3B2F730, desc: CHECKSUMS inprogress-off
lsn: 0/B3B2F858, prev 0/B3B2F810, desc: CHECKSUMS off
... crash / immediate restart ... (after 0/B3B3F430)
lsn: 0/B47DED08, desc: CHECKPOINT_SHUTDOWN redo 0/B47DED08; ...
checksums off; shutdown
lsn: 0/B5A7A600, prev 0/B5A788C8, desc: CHECKSUMS inprogress-on
The redo starts right after the checkpoint in 0/B28CA158. The failure
happens when trying to apply a FPW record to block in 1663/5/16409, but
the failure is actually in the associated VM, where block 4 has the
wrong checksum. How come?
Grepping the WAL, it seems the last time that VM block was changed is at
LSN 0/A1CF17F0 (which does match the failure log message):
lsn: 0/A1CEF7A0, ... desc: FPI , blkref #0: rel 1663/5/16409 fork vm blk
4 FPW
And AFAICS there are no other WAL records touching this particular vm
page (not even the XLOG_HEAP2_VISIBLE records).
Judging by the CHECKSUMS messages, the LSN 0/A1CEF7A0 is after the
cluster switched to the inprogress-on state, so at that point the
checksum should have been correct, right?
So how come the checksum is not correct when applying the heap page FPW
at LSN 0/B295E930? Either it got broken after enabling checksums in some
way, or it's the redo that got confused about the current "current"
checksum state.
It's a bit unfortunate we only detect invalid checksums if the failures
appear in the server log. I wonder if we could run pg_checksums after
the cluster gets shut down in 'fast' mode (we can't do that with
immediate shutdowns, of course). Didn't we do that in the past,
actually? I suppose there was a reason why we removed it, but maybe we
could still run that at least in some cases?
Anyway, let's assume the checksum was correct after enabling checksums.
That means it had to be broken by the redo, somehow? But why would that
be? The XLOG_CHECKSUMS record says we switched to "inprogress-off" at
0/B3B2F780, and "off" at 0/B3B2F858. Only then we stop updating the
checksums, right?
We "crash" shortly after LSN 0/B3B3F430 (because that's where the
immediate restart is, roughly), and there's a read-write pgbench running
in the background during all this. It might have modified the VM, but
why wouldn't that set the page LSN?
The second checksum failure is a bit more interesting - the VM page has
LSN 0/B29F5A20, which is *after* the redo location (and that checkpoint
has set "checksums on"). And the inprogress-off does not appear until
LSN 0/B3B2F780. So how come the VM page has invalid checksum?
I've only ever seen checksum failured on VM pages, never on any other
object. I have a feeling we're missing some important detail about how
visibility maps work ...
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-02-06 17:15 Tomas Vondra <[email protected]>
parent: Tomas Vondra <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Tomas Vondra @ 2026-02-06 17:15 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Andres Freund <[email protected]>; +Cc: Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi,
I spent a bit more time looking at this today, and I figured out a
simpler way to "cause trouble" by using PITR. I don't know if this has
the same root cause as the failures in the 006 TAP test, but I find it
interesting and I think it highlights some issues with the new patch.
The 006 test enables/disables checksums while running pgbench in the
background, and randomly kills the instance in different ways (restart
with fast/immediate mode). It then checks if the recovery hits any
checksum errors during redo. And every now and then it sees failures,
but it's tedious to investigate that because a lot can happen between
the checksum state change and the crash, and it's not clear at which
point it actually got broken.
I realized I can do a simpler thing. I can enable WAL archiving, run the
pgbench, enable/disable checksums, etc. And then I can do PITR into
different places in the WAL, possibly going record by record, and verify
checksums in every of those places. That should make it much more
deterministic than the "random" 006 test. And it really does.
So I did that, mostly like this:
1) setup instance with WAL archiving enabled
2) create basebackup
3) initialize pgbench
4) run read-write pgbench in the background
5) disable checksums
5) wait for data_checksums to change to "off"
6) stop the instance
Then I look for CHECKSUMS records in the WAL using pg_waldump, which can
tell look something like this:
...
lsn: 0/66707368, prev 0/66707318, desc: COMMIT 2026-02-06 ...
lsn: 0/66707390, prev 0/66707368, desc: CHECKSUMS inprogress-off
lsn: 0/667073B0, prev 0/66707390, desc: LOCK xmax: 48107, off: ...
...
lsn: 0/66715238, prev 0/66715200, desc: HOT_UPDATE old_xmax: ...
lsn: 0/66715288, prev 0/66715238, desc: CHECKSUMS off
lsn: 0/667152A8, prev 0/66715288, desc: HOT_UPDATE old_xmax: ...
...
And then do PITR to each of those LSNs (or any other interesting LSN)
using this:
recovery_target_lsn = '$LSN'
recovery_target_action = 'shutdown'
And once the instance shuts down, I can verify checksums on the data
directory using pg_checksums. And for these LSNs listed above I get:
0/66707368 - OK
0/66707390 - OK
0/667073B0 - OK
0/66715238 - OK
0/66715288 - 16155 failures
0/667152A8 - 15948 failures
There's a couple interesting details/questions here:
1) It seems a bit surprising we can run pg_checksums even after the
checksums flip to "off" at LSN 0/66715288. AFAICS this is a direct
consequence of separating this from checkpoints, but checkpoints are
still responsible for writing the state into the control file.
But during redo we don't generate new checkpoints, so we get into a
state when the control file still says "checksums on", but the data
files may already contain pages without correct checksums.
FWIW the other direction (when enabling checksums) can end up in a
similar "disagreement". The control file will still say "off" (or maybe
"inprogress-on") while the in-memory state will say "on". But I guess
that's harmless, as it won't cause checksum failures. Or maybe it can
cause some other issues, not sure.
I'm not sure what to do about this. The control file is updated only
lazily, but e.g. pg_checksums relies on it not being stale. Or at least
not stale "too much". The last patch ensured we have a checkpoint for
each state change, i.e. we can't go through both (on -> inprogress-off)
and (inprogress-off -> off) within a single checkpoint interval.
And that would prevent this issue, AFAIK. If we updated the control file
to say "inprogress-off" at some point, pg_checksums would know not to
try to verify checksums.
Maybe there are other issues, though. Having two places determining the
checksum state of an instance, and allowing them to get out of sync in
some way seems a bit tricky.
2) I don't understand how applying a single WAL record can trigger so
many checksum failures. Going from 0/66715238 to 0/66715288, which
applies the XLOG_CHECKSUMS record, triggered ~16k failures. How come?
That doesn't even touch any pages, AFAICS.
Similarly, applying the single HOT_UPDATE at 0/667152A8 (which per
pg_waldump touches only a single block) makes ~200 failures to go away.
I'm sure there is a simple explanation for this, but it's puzzling.
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-02-09 07:52 Michael Banck <[email protected]>
parent: Tomas Vondra <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: Michael Banck @ 2026-02-09 07:52 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi,
On Fri, Feb 06, 2026 at 12:15:27AM +0100, Tomas Vondra wrote:
> It's a bit unfortunate we only detect invalid checksums if the failures
> appear in the server log. I wonder if we could run pg_checksums after
> the cluster gets shut down in 'fast' mode (we can't do that with
> immediate shutdowns, of course).
Well, maybe we could do that with immediate shutdowns if we add a
--force parameter to pg_checksums that is clearly labelled/docmented as
being dangerous for running instances (but the instance wouldn't run in
this case). Or something undocumented like
--debug-force-unclean-controlfile.
Michael
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-11 23:56 Daniel Gustafsson <[email protected]>
parent: Tomas Vondra <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-03-11 23:56 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; +Cc: Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 6 Feb 2026, at 18:15, Tomas Vondra <[email protected]> wrote:
> I spent a bit more time looking at this today, and I figured out a
> simpler way to "cause trouble" by using PITR. I don't know if this has
> the same root cause as the failures in the 006 TAP test, but I find it
> interesting and I think it highlights some issues with the new patch.
I've encoded some of this into a new test suite in the attached patchset,
having it in a repeatable test should make it easier to reproduce and reason
about.
Attached is a rebase of the patchset which further refines the switch to using
XLOG_CHECKPOINT_* records which was proposed by Andres upstream. The errors I
encountered could be traced to replay across multiple state transitions (with
at least one case of ON->OFF), and re-introducing checkpoints at on and off
state transitions helped removing those.
* A lot of code has been moved out from xlog.c and into the file which contains
the code for the datachecksumsworker. This file has in turn been renamed to
signal what it's used for. It should probably be moved from backend/postmaster
but it's not clear to me where it should go.
* Improved signalhandling in the worker code.
* A new testsuite for PITR is added, which is based on Tomas' findings. This
can be further increased and refined but it's at least a start.
* Another new testsuite which changes full_page_writes settings is also added.
This one should perhaps be rolled into 001_basic, not sure.
* Tests 006 and 007, the pgbench "torture test" suites, are now being run in a
pared-down fashion when PG_TEST_EXTRA isn't defined, rather than being skipped
entirely
* The minimum max_wal_size in tests is now 32 instead of 64.
* Lots of polish and small tweaking and new comments
There are a few TODOs, the more interesting ones include:
* The change to XLOG_CHECKPOINT_REDO to move the wal_level into a proper record
structure should be pulled out as a 0001 patch as it's an cleanup that has
value on its own.
* Try to move even more code awy from xlog.c
* Address, and remove, all XXX comments (most of which stem from a much earlier
version of the patchset).
* The code for emitting, and waiting on, the procsignalbarrier exists in
multiple copies which needs to be refactored into a single one.
* Tests failed in Autoconf on CI, which I need to fix.
My next step will be addressing these TODOs, but I wanted to get a new version
on the list sooner rather than later.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260312-0001-Online-enabling-and-disabling-of-data-chec.patch (230.9K, ../../[email protected]/2-v20260312-0001-Online-enabling-and-disabling-of-data-chec.patch)
download | inline diff:
From fb173294b8dea5b1a640c406329e42baf1e01cb9 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 15 Aug 2025 14:48:02 +0200
Subject: [PATCH v20260312] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calcuated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests for online processing are gated begind the
PG_TEST_EXTRA flag to some degree due to being very time consuming
to run.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 77 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 235 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 12 +
doc/src/sgml/wal.sgml | 127 +-
src/backend/access/heap/heapam.c | 3 +-
src/backend/access/rmgrdesc/xlogdesc.c | 42 +-
src/backend/access/transam/xlog.c | 625 +++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 9 +
src/backend/postmaster/datachecksum_state.c | 1714 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/postmaster/walsummarizer.c | 5 +-
src/backend/replication/logical/decode.c | 1 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 13 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 4 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/include/access/xlog.h | 14 +-
src/include/access/xlog_internal.h | 13 +
src/include/catalog/pg_control.h | 6 +-
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/proc.h | 2 +-
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 81 +
.../test_checksums/t/006_pgbench_single.pl | 263 +++
.../test_checksums/t/007_pgbench_standby.pl | 385 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 172 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 28 +
.../modules/test_checksums/test_checksums.c | 222 +++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 9 +
76 files changed, 5308 insertions(+), 64 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 8cdd826fbd3..4c083335d89 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..46635d27e94 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,81 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums mode can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums mode to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all data checksums
+ have been calculated, and written, for all pages the cluster will
+ automatically set data checksums mode to <literal>on</literal>.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum mode to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum mode will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index e2db5bcc78c..d9378af0606 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index fd55b9ad23f..e805487344d 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index cc014564c97..b0dc99b5554 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3781,9 +3781,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3792,8 +3797,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7488,6 +7493,226 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on checkpoint</literal></entry>
+ <entry>
+ The command is currently waiting for a checkpoint to update the checksum
+ state before finishing.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index e9e393495df..e764b8be04d 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index d80dd46c5fd..26cf1c2868c 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject delays
+ and re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..df41fb54cb6 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases or tables.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,124 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will put the cluster checksum mode in
+ <literal>inprogress-on</literal> mode. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum mode will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in the application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> mode,
+ for any reason, then the checksum enable process must be restarted manually.
+ To do this, re-execute the function <function>pg_enable_data_checksums()</function>
+ once the cluster has been restarted. The process will start over, there is
+ no support for resuming work from where it was interrupted. If the cluster
+ is stopped while in <literal>inprogress-off</literal>, then the checksum
+ state will be set to <literal>off</literal> when the cluster is
+ restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum mode to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the mode will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing. There is no way to restart from where
+ processing was interrupted.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Rewriting all pages is not needed when disabling data checksums, but
+ checkpoints are still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 8f1c11a9350..021c140c18d 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -6018,8 +6018,7 @@ l4:
&new_xmax, &new_infomask, &new_infomask2);
if (PageIsAllVisible(BufferGetPage(buf)) &&
- visibilitymap_clear(rel, block, vmbuffer,
- VISIBILITYMAP_ALL_FROZEN))
+ visibilitymap_clear(rel, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN))
cleared_all_frozen = true;
START_CRIT_SECTION();
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index ff078f22264..8a854312ea1 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,25 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+static const char *
+get_checksum_version_string(ChecksumType checksum)
+{
+ switch (checksum)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +89,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +109,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_version_string(checkpoint->dataChecksumVersion),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -163,10 +185,12 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- int wal_level;
+ xl_checkpoint_redo xlrec;
- memcpy(&wal_level, rec, sizeof(int));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(wal_level));
+ memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_version_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -175,6 +199,13 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
memcpy(&enabled, rec, sizeof(bool));
appendStringInfoString(buf, enabled ? "true" : "false");
}
+ else if (info == XLOG_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_version_string(xlrec.new_checksumtype));
+ }
}
const char *
@@ -229,6 +260,9 @@ xlog_identify(uint8 info)
case XLOG_LOGICAL_DECODING_STATUS_CHANGE:
id = "LOGICAL_DECODING_STATUS_CHANGE";
break;
+ case XLOG_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
}
return id;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b9b678f3722..700d2b7459d 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -92,6 +92,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +554,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +654,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumType LocalDataChecksumVersion = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumVersion.
+ * See SetLocalDataChecksumVersion().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +736,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -831,9 +852,10 @@ XLogInsertRecord(XLogRecData *rdata,
* only happen just after a checkpoint, so it's better to be slow in
* this case and fast otherwise.
*
- * Also check to see if fullPageWrites was just turned on or there's a
- * running backup (which forces full-page writes); if we weren't
- * already doing full-page writes then go back and recompute.
+ * Also check to see if fullPageWrites was just turned on, there's a
+ * running backup or if checksums are enabled (all of which forces
+ * full-page writes); if we weren't already doing full-page writes
+ * then go back and recompute.
*
* If we aren't doing full-page writes then RedoRecPtr doesn't
* actually affect the contents of the XLOG record, so we'll update
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,362 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true iff data checksums are enabled or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksumsworker.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION ||
+ LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksumsworker.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
+ */
+bool
+DataChecksumsNeedVerify(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * DataChecksumsOnInProgress
+ * Returns whether data checksums are being enabled
+ *
+ * Most operations don't need to worry about the "inprogress" states, and
+ * should use DataChecksumsNeedVerify() or DataChecksumsNeedWrite(). The
+ * "inprogress-on" state for enabling checksums is used when the checksum
+ * worker is setting checksums on all pages, it can thus be used to check for
+ * aborted checksum processing which need to be restarted.
+ */
+bool
+DataChecksumsOnInProgress(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+}
+
+/*
+ * DataChecksumsOffInProgress
+ * Returns whether data checksums are being disabled
+ *
+ * The "inprogress-off" state for disabling checksums is used for when the
+ * worker resets the catalog state. DataChecksumsNeedVerify() or
+ * DataChecksumsNeedWrite() should be used for deciding whether to read/write
+ * checksums.
+ */
+bool
+DataChecksumsOffInProgress(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Enables data checksums cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksumsworker.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified. See datachecksumsworker.c for a longer discussion on
+ * how data checksums can be enabled in a running cluster.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition of "on" in all backends. When done we know that
+ * data checksums are enabled in all backends and data checksums are both
+ * written and verified.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalControlData
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumVersion(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumVersion(uint32 data_checksum_version)
+{
+ LocalDataChecksumVersion = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ switch (LocalDataChecksumVersion)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ /*
+ * Since all states are covered in the switch statement, this cannot
+ * happen.
+ */
+ Assert(false);
+ return "?";
}
/*
@@ -4925,6 +5298,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumVersion(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5468,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5547,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumVersion = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6624,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("data checksums state has been set to off"),
+ errhint("If checksums were being enabled during shutdown then processing must be manually restarted."));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6970,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7548,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7179,9 +7606,18 @@ CreateCheckPoint(int flags)
*/
if (!shutdown)
{
+ xl_checkpoint_redo redo_rec;
+
+ WALInsertLockAcquire();
+ redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+ WALInsertLockRelease();
+
/* Include WAL level in record for WAL summarizer's benefit. */
XLogBeginInsert();
- XLogRegisterData(&wal_level, sizeof(wal_level));
+ XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
/*
@@ -7237,6 +7673,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7386,6 +7826,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7529,6 +7972,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7870,6 +8319,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumVersion;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8284,6 +8737,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksumtype = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG_ID, XLOG_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8410,6 +8881,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+ SetLocalDataChecksumVersion(checkPoint.dataChecksumVersion);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8469,6 +8945,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
LWLockRelease(ControlFileLock);
/*
@@ -8495,6 +8972,9 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
+ uint64 barrier;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8533,6 +9013,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8544,6 +9026,42 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * XXX: I'm not sure this is right, and if it is this needs
+ * refactoring to be pretty (as well as in the _REDO case
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+ if (checkPoint.dataChecksumVersion != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ {
+ switch (checkPoint.dataChecksumVersion)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8701,7 +9219,43 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+ uint64 barrier;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ {
+ switch (redo_rec.data_checksum_version)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8752,6 +9306,51 @@ xlog_redo(XLogReaderState *record)
}
}
}
+ else if (info == XLOG_CHECKSUMS)
+ {
+ xl_checksum_state state;
+ uint64 barrier;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ /*
+ * XXX Could this end up written to the control file prematurely? IIRC
+ * that happens during checkpoint, so what if that gets triggered e.g.
+ * because someone runs CHECKPOINT? If we then crash (or something
+ * like that), could that confuse the instance?
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksumtype;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ switch (state.new_checksumtype)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
}
/*
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..cdbaa0c0337 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point were we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 339c016e510..05bd7737caf 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1434,6 +1434,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..9803ece0792 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumVersion();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 0104a86b9ec..26a4972b84e 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -143,6 +144,14 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
}
};
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..45e36226c94
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1714 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a database at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads wont fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of databases which exist at the
+ * start of checksumming, and once all are processed will regenerate the list
+ * and start over processing any new entries. Once there are no new entries on
+ * the list, processing will end. All databases MUST BE successfully processed
+ * in order for data checksums to be enabled, the only exception are databases
+ * which are dropped before having been processed.
+
+ * Any new relation in a processed database, created during processing, will
+ * see the in-progress state and will automatically be checksummed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer verified. This ensure
+ * that backends which have yet to move from the "on" state will still be able
+ * to process data checksum validation.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling, or disabling, data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers, before
+ * updating the controlfile with the state all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one set being empty:
+ *
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set of "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksums_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the set of conditions to use is
+ * looked up in the checksum_barriers struct. The struct member for the target
+ * state defines which state the backend must currently be in, and which it
+ * must not be in.
+ *
+ * The reason for this explicit checking is to ensure that processing cannot
+ * be started such that it breaks the assumptions of the state machine. See
+ * datachecksumsworker.c for a lengthy discussion on these states.
+ *
+ * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
+ * barrier_ne in the below checksum_barriers definition.
+ */
+#define MAX_BARRIER_CONDITIONS 2
+typedef struct ChecksumBarrierCondition
+{
+ /* The target state of the barrier */
+ int target;
+ /* A set of states in which at least one MUST match the current state */
+ int barrier_eq[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_eq set */
+ int barrier_eq_sz;
+ /* A set of states which all MUST NOT match the current state */
+ int barrier_ne[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_ne set */
+ int barrier_ne_sz;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[4] =
+{
+ {PG_DATA_CHECKSUM_OFF, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION}, 2, {PG_DATA_CHECKSUM_VERSION}, 1},
+ {PG_DATA_CHECKSUM_VERSION, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION}, 1, {0}, 0},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, {0}, 0, {PG_DATA_CHECKSUM_VERSION}, 1},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, {PG_DATA_CHECKSUM_VERSION}, 1, {0}, 0},
+};
+
+/*
+ * Number of times we retry to open a database before giving up and consider
+ * it to have failed processing.
+ */
+#define DATACHECKSUMSWORKER_MAX_DB_RETRIES 5
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsWorkerShmemStruct
+{
+ /*
+ * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * If a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ bool worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsWorkerShmemStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+typedef struct DataChecksumsWorkerResultEntry
+{
+ Oid dboid;
+ DataChecksumsWorkerResult result;
+ int retries;
+} DataChecksumsWorkerResultEntry;
+
+
+/*
+ * Flag set by the interrupt handler
+ */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/*
+ * Are we enabling data checksums, or disabling them?
+ */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ *
+ */
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ const ChecksumBarrierCondition *condition = NULL;
+ int target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumVersion(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers); i++)
+ {
+ if (checksum_barriers[i].target == target_state)
+ condition = &checksum_barriers[i];
+ }
+ Assert(condition);
+
+ /*
+ * The current state MUST be equal to one of the EQ states defined in this
+ * barrier condition. If the EQ states array is zero then that implies
+ * that the current state can match any state, so fastpath check for that
+ * first.
+ */
+ if (condition->barrier_eq_sz == 0)
+ found = true;
+ else
+ {
+ for (int i = 0; i < condition->barrier_eq_sz; i++)
+ {
+ if (current == condition->barrier_eq[i])
+ found = true;
+ }
+ }
+
+ /*
+ * The current state MUST NOT be equal to any of the NE states defined in
+ * this barrier condition.
+ */
+ for (int i = 0; i < condition->barrier_ne_sz; i++)
+ {
+ if (current == condition->barrier_ne[i])
+ found = false;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumVersion(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ *
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumsWorkerShmem->launch_operation = op;
+ DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
+ DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumsWorkerShmem->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ if (!relns)
+ return false;
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %dblocks)",
+ relns, RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock before we can flag as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the master happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here. It's safe to check this without
+ * a lock, because if we miss it being set, we will try again soon.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /*
+ * As of now we only update the block counter for main forks in order
+ * to not cause too frequent calls. TODO: investigate whether we
+ * should do it more frequent?
+ */
+ if (forkNum == MAIN_FORKNUM)
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ pfree(relns);
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+ elog(DEBUG2,
+ "data checksum processing done for relation with OID %u: %s",
+ relationId, (aborted ? "aborted" : "finished"));
+
+ CommitTransactionCommand();
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, make sure we retry processing
+ * this database. This will make the datachecksumsworker move on to the
+ * next database and quite likely fail with the same problem. TODO: Maybe
+ * we need a backoff to avoid running through all the databases here in
+ * short order.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("failed to start worker for enabling data checksums in database \"%s\", retrying",
+ db->dbname),
+ errhint("The max_worker_processes setting might be too low."));
+ return DATACHECKSUMSWORKER_RETRYDB;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the pg_control version to enabled so
+ * when the cluster comes back up processing will have to be restarted.
+ * When disabling, the pg_control version will be set to off before this
+ * so when the cluster comes up checksums will be off as expected.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ DataChecksumsWorkerShmem->worker_running = true;
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ DataChecksumsWorkerShmem->worker_running = false;
+
+ return DataChecksumsWorkerShmem->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+
+ /*
+ * TODO: how to really handle the worker still running when the
+ * launcher exits?
+ */
+ if (DataChecksumsWorkerShmem->worker_running)
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running"));
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumsWorkerShmem->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /*
+ * Initialize a connection to shared catalogs only.
+ */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->launcher_running = true;
+ DataChecksumsWorkerShmem->operation = operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ * Hold interrupts to make sure state doesn't change during checking.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ }
+ else
+ {
+ Assert(false);
+ }
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will repeatedly generate a list of databases to process for enabling
+ * checksums. Until no new databases are found, this will loop around computing
+ * a new list and comparing it to the already seen ones.
+ *
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ HTAB *ProcessedDatabases = NULL;
+ HASHCTL hash_ctl;
+ bool found_failed = false;
+
+ /* Initialize a hash tracking all processed databases */
+ memset(&hash_ctl, 0, sizeof(hash_ctl));
+ hash_ctl.keysize = sizeof(Oid);
+ hash_ctl.entrysize = sizeof(DataChecksumsWorkerResultEntry);
+ ProcessedDatabases = hash_create("Processed databases",
+ 64,
+ &hash_ctl,
+ HASH_ELEM | HASH_BLOBS);
+
+ /*
+ * Set up so first run processes shared catalogs, but not once in every
+ * db.
+ */
+ DataChecksumsWorkerShmem->process_shared_catalogs = true;
+
+ /*
+ * Get a list of all databases to process. This may include databases that
+ * were created during our runtime. Since a database can be created as a
+ * copy of any other database (which may not have existed in our last
+ * run), we have to repeat this loop until no new databases show up in the
+ * list. Here the initial list for the loop processing is generated after
+ * waiting for all existing transactions to finish to ensure that we can
+ * see any database which was created even if the transaction in which it
+ * was created started before checksums were being processed.
+ */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /* Allow a test case to modify the initial list of databases */
+ INJECTION_POINT("datachecksumsworker-initial-dblist", DatabaseList);
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ while (true)
+ {
+ int processed_databases = 0;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+ DataChecksumsWorkerResultEntry *entry;
+ bool found;
+
+ /*
+ * Check if this database has been processed already, and if so
+ * whether it should be retried or skipped.
+ */
+ entry = (DataChecksumsWorkerResultEntry *) hash_search(ProcessedDatabases, &db->dboid,
+ HASH_FIND, NULL);
+
+ if (entry)
+ {
+ if (entry->result == DATACHECKSUMSWORKER_RETRYDB)
+ {
+ /*
+ * Limit the number of retries to avoid infinite looping
+ * in case there simply won't be enough workers in the
+ * cluster to finish this operation.
+ */
+ if (entry->retries > DATACHECKSUMSWORKER_MAX_DB_RETRIES)
+ entry->result = DATACHECKSUMSWORKER_FAILED;
+ }
+
+ /* Skip if this database has been processed already */
+ if (entry->result != DATACHECKSUMSWORKER_RETRYDB)
+ continue;
+ }
+
+ result = ProcessDatabase(db);
+ processed_databases++;
+
+ /*
+ * Update the number of processed databases in the progress
+ * report.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ processed_databases);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-fail-db", &result);
+
+ if (result == DATACHECKSUMSWORKER_SUCCESSFUL)
+ {
+ /*
+ * If one database has completed shared catalogs, we don't
+ * have to process them again.
+ */
+ if (DataChecksumsWorkerShmem->process_shared_catalogs)
+ DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ entry = hash_search(ProcessedDatabases, &db->dboid, HASH_ENTER, &found);
+ entry->dboid = db->dboid;
+ entry->result = result;
+ if (!found)
+ entry->retries = 0;
+ else
+ entry->retries++;
+ }
+
+ elog(DEBUG1,
+ "%i databases processed for data checksum enabling, %s",
+ processed_databases,
+ (processed_databases ? "process with restart" : "process completed"));
+
+ FreeDatabaseList(DatabaseList);
+
+ /*
+ * If no databases were processed in this run of the loop, we have now
+ * finished all databases and no concurrently created ones can exist.
+ */
+ if (processed_databases == 0)
+ break;
+
+ /*
+ * Re-generate the list of databases for another pass in case any new
+ * databases were created while we were running. Since the initial
+ * list was generated after waiting for all transaction to finish we
+ * know that all new databases found here must have been created while
+ * seeing the new checksum state. By waiting for all transactions
+ * here as well we know that any database created using an existing db
+ * as a template (which may have been used before it had checksums
+ * enabled) will be committed.
+ */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+ }
+
+ /*
+ * ProcessedDatabases now has all databases and the results of their
+ * processing. Failure to enable checksums for a database can be because
+ * they actually failed for some reason, or because the database was
+ * dropped between us getting the database list and trying to process it.
+ * Get a fresh list of databases to detect the second case where the
+ * database was dropped before we had started processing it. If a database
+ * still exists, but enabling checksums failed then we fail the entire
+ * checksumming process and exit with an error.
+ */
+ DatabaseList = BuildDatabaseList();
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResultEntry *entry;
+ bool found;
+
+ entry = hash_search(ProcessedDatabases, (void *) &db->dboid,
+ HASH_FIND, &found);
+
+ /*
+ * We are only interested in the processed databases which failed, and
+ * where the failed database still exists. This indicates that
+ * enabling checksums actually failed, and not that the failure was
+ * due to the db being concurrently dropped.
+ */
+ if (found && entry->result == DATACHECKSUMSWORKER_FAILED)
+ {
+ ereport(WARNING,
+ errmsg("failed to enable data checksums in \"%s\"", db->dbname));
+ found_failed = found;
+ continue;
+ }
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ if (found_failed)
+ {
+ /* Disable checksums on cluster, because we failed */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumsWorkerShmemSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsWorkerShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumsWorkerShmemInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsWorkerShmemInit(void)
+{
+ bool found;
+
+ DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsWorkerShmemSize(),
+ &found);
+
+ if (!found)
+ MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /*
+ * Only include temporary relations when asked for a temp relation
+ * list.
+ */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
+ VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumsWorkerShmem->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3fac46c402b..c898c53b623 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2999,6 +2999,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add checksumming processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c
index e1aa102f41d..bdc2236a8c1 100644
--- a/src/backend/postmaster/walsummarizer.c
+++ b/src/backend/postmaster/walsummarizer.c
@@ -1428,8 +1428,11 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward)
if (info == XLOG_CHECKPOINT_REDO)
{
+ xl_checkpoint_redo xlrec;
+
/* Payload is wal_level at the time record was written. */
- memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int));
+ memcpy(&xlrec, XLogRecGetData(xlogreader), sizeof(xl_checkpoint_redo));
+ record_wal_level = xlrec.wal_level;
}
else if (info == XLOG_CHECKPOINT_SHUTDOWN)
{
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 21f03864a66..0d0caf07361 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -181,6 +181,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
case XLOG_FPW_CHANGE:
case XLOG_FPI_FOR_HINT:
case XLOG_FPI:
+ case XLOG_CHECKSUMS:
case XLOG_OVERWRITE_CONTRECORD:
case XLOG_CHECKPOINT_REDO:
break;
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index a4785daf1e5..bbd59d8af44 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsWorkerShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -316,6 +318,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsWorkerShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index de85911e3ac..8da8f8fecf1 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,8 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +119,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -1511,7 +1513,7 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
static char *pageCopy = NULL;
/* If we don't need a checksum, just return the passed-in data */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
return page;
/*
@@ -1540,9 +1542,14 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
void
PageSetChecksumInplace(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index f2f8d3ff75f..07fb6bacb40 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..8105a0af79b 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,9 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_FINISHCONDITION "Waiting for data checksums to be enabled."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -366,6 +369,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5ac022274a7..4cd06057042 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -296,6 +296,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1180,9 +1182,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1198,9 +1197,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index b59e08605cc..b505db83cca 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -757,6 +757,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumVersion();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -885,7 +903,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index a5a0edf2534..fbd415a1b8b 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -531,11 +531,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38aaf82f120..661b8757979 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -628,7 +636,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e686d88afc4..0a258e76b83 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -540,11 +540,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..47afa82d9c5 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumVersion);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index fdfb572467b..788983dfe21 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -242,7 +243,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsOnInProgress(void);
+extern bool DataChecksumsOffInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern void InitLocalDataChecksumVersion(void);
+extern void SetLocalDataChecksumVersion(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 58ae12bb20f..b53c10cfb70 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumType new_checksumtype;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -303,6 +310,12 @@ typedef struct xl_end_of_recovery
int wal_level;
} xl_end_of_recovery;
+typedef struct xl_checkpoint_redo
+{
+ int wal_level;
+ uint32 data_checksum_version;
+} xl_checkpoint_redo;
+
/*
* The functions in xloginsert.c construct a chain of XLogRecData structs
* to represent the final WAL record.
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 7503db1af51..7290ece5205 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumVersion;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -82,6 +85,7 @@ typedef struct CheckPoint
#define XLOG_OVERWRITE_CONTRECORD 0xD0
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+#define XLOG_CHECKSUMS 0xC0
/*
@@ -223,7 +227,7 @@ typedef struct ControlFileData
bool float8ByVal; /* float8, int8, etc pass-by-value? */
/* Are data pages protected by checksums? Zero if no checksum version */
- uint32 data_checksum_version;
+ uint32 data_checksum_version; /* persistent */
/*
* True if the default signedness of char is "signed" on a platform where
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..8313b1dbd04 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12531,6 +12531,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 's', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..fb4fca24991 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..5601241a803
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsWorkerShmemSize(void);
+extern void DataChecksumsWorkerShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+ /* TODO: VERIFY_DATACHECKSUMS, */
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_RETRYDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index 3de58ba4312..ed7e0abcbfa 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..b6bbba82949 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum version 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_VERSION defines that data checksums are enabled in the
+ * cluster and PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF}_VERSION defines that data
+ * checksums are either currently being enabled or disabled.
+ */
+typedef enum ChecksumType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION
+} ChecksumType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index e94ebce95b9..0c02f2ae38f 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 3f89450c216..bd53d20e631 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -527,7 +527,7 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs;
* Background writer, checkpointer, WAL writer, WAL summarizer, and archiver
* run during normal operation. Startup process and WAL receiver also consume
* 2 slots, but WAL writer is launched only after startup has exited, so we
- * only need 6 slots.
+ * only need 6 slots to cover these.
*/
#define MAX_IO_WORKERS 32
#define NUM_AUXILIARY_PROCS (6 + MAX_IO_WORKERS)
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4ac5c84db43..88e2ffd8996 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index e2b3eef4136..aab7aa00f82 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..397f6411cb5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 'on');
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..a4d35e9f95c
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksumming to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..c5732a2c7da
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby_1->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby_1->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby_1, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby_1, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby_1->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby_1->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby_1, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby_1, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby_1->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f33e278de28
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksumming to block on, in this case a pre-existing
+# temporary table which is kept open while processing is started. We can
+# accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..5f7f4102b9e
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+# Force the enable checksums processing to make multiple passes by removing
+# one database from the list in the first pass. This will simulate a CREATE
+# DATABASE during processing. Doing this via fault injection makes the test
+# not be subject to exact timing.
+$node->safe_psql('postgres', 'SELECT dcw_prune_dblist(true);');
+enable_data_checksums($node, wait => 'on');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..727c804bbd5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,263 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test is very expensive to run, so by default it runs a pared-down test
+# suite. Setting $extended to 1 will run the full suite and can be done by
+# setting PG_TEST_EXTRA to "checksum_extended".
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+$node->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..8e099fdbe65
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,385 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby_1;
+my $node_standby_1_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+
+ # Terminate any currently running pgbench process before continuing
+ $pgbench_primary->finish if $pgbench_primary;
+
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ my $clients = 1;
+ my $runtime = 10;
+
+ if ($extended)
+ {
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench_primary = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby_1, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby_1->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby_1->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby_1, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby_1, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby_1, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby_1->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby_1->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby_1->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby_1->is_alive)
+ {
+ $node_standby_1->start;
+ $node_standby_1->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby_1->append_conf('postgresql.conf',
+ qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby_1->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby_1->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby_1, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently)
+ # XXX should maybe try stopping nodes in the opposite order too?
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby_1->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby_1->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_1_loglocation = -s $node_standby_1->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby_1->is_alive)
+{
+ $node_standby_1->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby_1, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+$node_standby_1->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..6c98e0ab23d
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,172 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/)
+{
+ plan skip_all => 'Extended checksum tests not enabled';
+}
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ my $clients = 1 + int(rand(15));
+
+ my @cmd = ('pgbench', '-p', $port, '-T', '600', '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if (cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('node_pitr');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..778c696abc6
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 'off');
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..fb1c802461b
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,28 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_prune_dblist(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..c75e9d746c3
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,222 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_dblist(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+dc_fail_database(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+
+ if (first_pass)
+ *res = DATACHECKSUMSWORKER_FAILED;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fail-db",
+ "test_checksums",
+ "dc_fail_database",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fail-db");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to remove an entry from the Databaselist to force re-processing since
+ * not all databases could be processed in the first iteration of the loop.
+ */
+void
+dc_dblist(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ List *DatabaseList = (List *) arg;
+
+ if (first_pass)
+ DatabaseList = list_delete_last(DatabaseList);
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_prune_dblist);
+Datum
+dcw_prune_dblist(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-initial-dblist",
+ "test_checksums",
+ "dc_dblist",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-initial-dblist");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..8b3c4af3284 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3863,6 +3863,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f373ad704b6..3fd41b8d5bb 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2078,6 +2078,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index cd00f35bf7a..aa86c11bd99 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3da19d41413..6a3d0e7f83c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -429,6 +429,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumType
Chromosome
CkptSortItem
CkptTsStatus
@@ -599,6 +601,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -617,6 +620,10 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsWorkerResultEntry
+DataChecksumsWorkerShmemStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4330,6 +4337,8 @@ xl_btree_split
xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
+xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-15 22:47 Tomas Vondra <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 2 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-03-15 22:47 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
On 3/12/26 00:56, Daniel Gustafsson wrote:
>> On 6 Feb 2026, at 18:15, Tomas Vondra <[email protected]> wrote:
>
>> I spent a bit more time looking at this today, and I figured out a
>> simpler way to "cause trouble" by using PITR. I don't know if this has
>> the same root cause as the failures in the 006 TAP test, but I find it
>> interesting and I think it highlights some issues with the new patch.
>
> I've encoded some of this into a new test suite in the attached patchset,
> having it in a repeatable test should make it easier to reproduce and reason
> about.
>
> Attached is a rebase of the patchset which further refines the switch to using
> XLOG_CHECKPOINT_* records which was proposed by Andres upstream. The errors I
> encountered could be traced to replay across multiple state transitions (with
> at least one case of ON->OFF), and re-introducing checkpoints at on and off
> state transitions helped removing those.
>
> * A lot of code has been moved out from xlog.c and into the file which contains
> the code for the datachecksumsworker. This file has in turn been renamed to
> signal what it's used for. It should probably be moved from backend/postmaster
> but it's not clear to me where it should go.
> * Improved signalhandling in the worker code.
> * A new testsuite for PITR is added, which is based on Tomas' findings. This
> can be further increased and refined but it's at least a start.
> * Another new testsuite which changes full_page_writes settings is also added.
> This one should perhaps be rolled into 001_basic, not sure.
> * Tests 006 and 007, the pgbench "torture test" suites, are now being run in a
> pared-down fashion when PG_TEST_EXTRA isn't defined, rather than being skipped
> entirely
> * The minimum max_wal_size in tests is now 32 instead of 64.
> * Lots of polish and small tweaking and new comments
>
> There are a few TODOs, the more interesting ones include:
>
> * The change to XLOG_CHECKPOINT_REDO to move the wal_level into a proper record
> structure should be pulled out as a 0001 patch as it's an cleanup that has
> value on its own.
Makes sense, but it's going to be harder because since d774072f0040 all
4 bits in XLR_INFO are used.
> * Try to move even more code awy from xlog.c
> * Address, and remove, all XXX comments (most of which stem from a much earlier
> version of the patchset).
> * The code for emitting, and waiting on, the procsignalbarrier exists in
> multiple copies which needs to be refactored into a single one.
> * Tests failed in Autoconf on CI, which I need to fix.
>
A couple minor issues I noticed:
1) Is this actually doing the expected thing?
INJECTION_POINT("datachecksumsworker-initial-dblist", DatabaseList);
We're passing a regular pointer to the database list, so can the
injection point actually modify it? I suppose it happens to work because
dc_dblist() removes the last item, so the pointer to the list does not
change. But that's luck.
2) ProcessAllDatabases may be misusing processed_databases
The loop is using processed_databases to track number of databases
processed in each interation, but it's also being used for progress
reporting through pgstat_progress_update_param(). So if we have to go
through multiple iterations of the loop, it'll reset to 0 and then only
count newly processed ones. But the progress still shows the original
total number of databases. IMO it needs to track the cumulative number
of processed databases.
3) DATACHECKSUMSWORKER_MAX_DB_RETRIES / DATACHECKSUMSWORKER_FAILED
What happens if a database reaches the maximum number of retries? We
mark that entry as failed, but AFAIK we'll still try to process any
remaining databases. Isn't that already doomed and we won't be able to
enable checksums? So why not to simply abort the loop right away?
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-15 22:56 Tomas Vondra <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-03-15 22:56 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
On 3/12/26 23:56, Daniel Gustafsson wrote:
>> On 12 Mar 2026, at 00:56, Daniel Gustafsson <[email protected]> wrote:
>
> One thing I forgot to mention was that this patch uncovered the same vmap (and
> fsm) issue as is being discussed in [0]. At the time I didn't connect the dots
> that it was an issue even without this patch so I tried to solve it as part of
> this work. After speaking with Andres I ripped out my attempts and will let
> the other thread take care of the issue independently of this.
>
Yeah, this is quite annoying. It makes 100% sense to wait for the
visibility map to get fixed (by properly WAL-logging it). But I'm not
aware of any such fixes for the FSM though, so I what's the plan for
that? IMO the right solution would be to do proper WAL for FSM, but that
seems like a significant amount of work.
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-15 22:59 Tomas Vondra <[email protected]>
parent: Tomas Vondra <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-03-15 22:59 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
On 3/15/26 23:47, Tomas Vondra wrote:
> On 3/12/26 00:56, Daniel Gustafsson wrote:
>> ...
>>
>> * The change to XLOG_CHECKPOINT_REDO to move the wal_level into a proper record
>> structure should be pulled out as a 0001 patch as it's an cleanup that has
>> value on its own.
>
> Makes sense, but it's going to be harder because since d774072f0040 all
> 4 bits in XLR_INFO are used.
>
Correction. This is not a problem for the XLOG_CHECKPOINT_REDO patch,
but for adding XLOG_CHECKSUMS. I guess we'll need a second xlog rmgr.
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-17 22:01 Daniel Gustafsson <[email protected]>
parent: Tomas Vondra <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-03-17 22:01 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 17 Mar 2026, at 12:45, Heikki Linnakangas <[email protected]> wrote:
>
> Here's a quick, non-exhaustive review of v20260316 of the patch. I haven't followed the thread, so forgive me if some of these things have already been discussed.
>
> I don't see any major issues, just a bunch of small things:
Thanks you so much for looking!
>> +/*
>> + * Checksum version 0 is used for when data checksums are disabled (OFF).
>> + * PG_DATA_CHECKSUM_VERSION defines that data checksums are enabled in the
>> + * cluster and PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF}_VERSION defines that data
>> + * checksums are either currently being enabled or disabled.
>> + */
>> +typedef enum ChecksumType
>> +{
>> + PG_DATA_CHECKSUM_OFF = 0,
>> + PG_DATA_CHECKSUM_VERSION,
>> + PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION,
>> + PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION
>> +} ChecksumType;
>
> Naming: This is called "VERSION", but also "Type", and I'm a little confused. I think the field was called "version" because we envisioned that we might want to use a different checksum algorithm in the future, which could then be indicated by a different checksum version number. If we do that in the future, how will these values look like?
This is an old leftover the patch has carried for too long, the inprogress
states were named _VERSION simply due to the ON state being so. I've renamed
them to just INPROGRESS_{ON|OFF} and made sure to place the ON state as the
last element in the enum. If we add other checksum algorithms in the future
there are different naming schemes we could use depending on which level of
implementation abstraction we want. We can either append a version number (and
consider the current ON state to be version 1 or 0), or an algorithm name (with
the current ON state a defuault algorithm).
* PG_DATA_CHECKSUM_<alg>_VERSION
* PG_DATA_CHECKSUM_VERSION_<n>
To further reduce this naming confusing I have done the following:
s/LocalDataChecksumVersion/LocalDataChecksumState/
s/ChecksumType/ChecksumStateType/
s/xl_checksum_state->new_checksumtype/xl_checksum_state->new_checksum_state/
s/Checkpoint->dataChecksumVersion/Checkpoint->dataChecksumState/
>> @@ -831,9 +852,10 @@ XLogInsertRecord(XLogRecData *rdata,
>> * only happen just after a checkpoint, so it's better to be slow in
>> * this case and fast otherwise.
>> *
>> - * Also check to see if fullPageWrites was just turned on or there's a
>> - * running backup (which forces full-page writes); if we weren't
>> - * already doing full-page writes then go back and recompute.
>> + * Also check to see if fullPageWrites was just turned on, there's a
>> + * running backup or if checksums are enabled (all of which forces
>> + * full-page writes); if we weren't already doing full-page writes
>> + * then go back and recompute.
>> *
>> * If we aren't doing full-page writes then RedoRecPtr doesn't
>> * actually affect the contents of the XLOG record, so we'll update
>
> It's true that if checksums are enabled, we must do full-page writes. But that's already included in the "fullpageWrites" flag. We're not specifically checking whether checksums are enabled here, so I find this comment change more confusing than helpful.
Fair enough, removed.
>> void
>> SetDataChecksumsOnInProgress(void)
>> {
>> uint64 barrier;
>> Assert(ControlFile != NULL);
>> /*
>> * The state transition is performed in a critical section with
>> * checkpoints held off to provide crash safety.
>> */
>> START_CRIT_SECTION();
>> MyProc->delayChkptFlags |= DELAY_CHKPT_START;
>> XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
>> SpinLockAcquire(&XLogCtl->info_lck);
>> XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
>> SpinLockRelease(&XLogCtl->info_lck);
>> barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
>> MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
>> END_CRIT_SECTION();
>> /*
>> * Update the controlfile before waiting since if we have an immediate
>> * shutdown while waiting we want to come back up with checksums enabled.
>> */
>> LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
>> ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
>> UpdateControlFile();
>> LWLockRelease(ControlFileLock);
>> /*
>> * Await state change in all backends to ensure that all backends are in
>> * "inprogress-on". Once done we know that all backends are writing data
>> * checksums.
>> */
>> WaitForProcSignalBarrier(barrier);
>> }
>
> If a checkpoint starts, finishes, and you then crash, all between the END_CRIT_SECTION() and LWLockAcquire here, are you left with with wrong 'data_checksum_version' in the control file? (Highly theoretical, the window is minuscule, but still)
Thats true. In that case the REDO record should have the right value though so
during replay it will be handled.
>> +/*
>> + * InitLocalControlData
>> + *
>> + * Set up backend local caches of controldata variables which may change at
>> + * any point during runtime and thus require special cased locking. So far
>> + * this only applies to data_checksum_version, but it's intended to be general
>> + * purpose enough to handle future cases.
>> + */
>> +void
>> +InitLocalDataChecksumVersion(void)
>> +{
>> + SpinLockAcquire(&XLogCtl->info_lck);
>> + SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
>> + SpinLockRelease(&XLogCtl->info_lck);
>> +}
>
> Function name in the comment doesn't match.
Fixed.
>> + ereport(WARNING,
>> + errmsg("data checksums state has been set to off"),
>> + errhint("If checksums were being enabled during shutdown then processing must be manually restarted."));
>
> This is a little unclear. If we reach this, we know that the checkum calculation was interrupted, no need to caveat it with "if checksums were being enabled", right? Maybe something like:
>
> WARNING: enabling data checksums was interrupted
> HINT: The checksum processing must be manually restarted
>
> I feel we're missing a term for the process of enabling checkums. "checksum enablement"? "initial checksum calculation"? In some comments it's called "checksumming".
I'm not a fan of "checksumming" so I've gone over all occurrences and reworded
them.
>> * The DataChecksumsWorker will compile a list of databases which exist at the
>> * start of checksumming, and once all are processed will regenerate the list
>> * and start over processing any new entries. Once there are no new entries on
>> * the list, processing will end. All databases MUST BE successfully processed
>> * in order for data checksums to be enabled, the only exception are databases
>> * which are dropped before having been processed.
>
> Why does the list need to be regenerated? Any newly created databases would already have checksums enabled, right? Is this because you might use database that hadn't been fully processed yet as the template? Would be good to mention if that's the reason.
Correct, we cannot rely on the initial list being the full list. Comment
expanded.
> Could CREATE DATABASE calculate the checksums when it copies the files?
I guess it could, though I think that should be a separate patch as this is
already (too) big. It's a good idea though, added to the "ideas for future
optimizations" section in the documentation comment.
>> * means that connected backends will change state at different times. If
>> * waiting for a barrier is done during startup, for example during replay, it
>> * is important to realize that any locks held by the startup process might
>> * cause deadlocks if backends end up waiting for those locks while startup
>> * is waiting for a procsignalbarrier.
>
> Don't we absorb procsignalbarriers while waiting on a lock? Or does this mean LWlocks?
It is referring to LWlocks.
>> * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
>> * DataChecksumsLauncher. When all backends have acknowledged the barrier then
>> * Bd will be empty and the next phase can begin: calculating and writing data
>> * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
>> * have finished writing checksums on all pages, data checksums are enabled
>> * cluster-wide via another procsignalbarrier. There are four sets of backends
>> * where Bd shall be an empty set:
>> *
>> * Bg: Backend updating the global state and emitting the procsignalbarrier
>> * Bd: Backends in "off" state
>> * Be: Backends in "on" state
>> * Bi: Backends in "inprogress-on" state
>> *
>> * Backends in Bi and Be will write checksums when modifying a page, but only
>> * backends in Be will verify the checksum during reading. The Bg backend is
>> * blocked waiting for all backends in Bi to process interrupts and move to
>> * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
>> * observe the global state being "on" and will thus automatically belong to
>> * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
>> * are compatible sets while still operating based on their local state as
>> * both write data checksums.
>
> Isn't "Bg" always the data checksums launcher process? In the previous paragraph before this, you didn't list the data checksums launcher as a separate "set".
Good catch, added.
>> /*
>> * Configuration of conditions which must match when absorbing a procsignal
>> * barrier during data checksum enable/disable operations. A single function
>> * is used for absorbing all barriers, and the set of conditions to use is
>> * looked up in the checksum_barriers struct. The struct member for the target
>> * state defines which state the backend must currently be in, and which it
>> * must not be in.
>> *
>> * The reason for this explicit checking is to ensure that processing cannot
>> * be started such that it breaks the assumptions of the state machine. See
>> * datachecksumsworker.c for a lengthy discussion on these states.
>
> This is "datachecksumworker.c", aka "datachecksum_state.c", so this "See datachecksumworker.c" refers to itself.
Ugh, I missed fixing this when I recently moved code out of xlog.c. Fixed.
>> *
>> * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
>> * barrier_ne in the below checksum_barriers definition.
>> */
>> #define MAX_BARRIER_CONDITIONS 2
>> typedef struct ChecksumBarrierCondition
>> {
>> /* The target state of the barrier */
>> int target;
>> /* A set of states in which at least one MUST match the current state */
>> int barrier_eq[MAX_BARRIER_CONDITIONS];
>> /* The number of elements in the barrier_eq set */
>> int barrier_eq_sz;
>> /* A set of states which all MUST NOT match the current state */
>> int barrier_ne[MAX_BARRIER_CONDITIONS];
>> /* The number of elements in the barrier_ne set */
>> int barrier_ne_sz;
>> } ChecksumBarrierCondition;
>
> I don't understand the logic here. Why do you need both 'barrier_eq' and 'barrier_ne'? Wouldn't it be enough to just list the allowed states, and everything else is not allowed?
It was in an attempt to make it more readable, to avoid long lists of states
which look quite similar. It could be handled with just a set of required
allowed states if that's what is preferred, I don't have strong opinions myself
really.
>> static const ChecksumBarrierCondition checksum_barriers[4] =
>> {
>> {PG_DATA_CHECKSUM_OFF, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION}, 2, {PG_DATA_CHECKSUM_VERSION}, 1},
>> {PG_DATA_CHECKSUM_VERSION, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION}, 1, {0}, 0},
>> {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, {0}, 0, {PG_DATA_CHECKSUM_VERSION}, 1},
>> {PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, {PG_DATA_CHECKSUM_VERSION}, 1, {0}, 0},
>> };
>
> This is pretty jarring to read. Maybe use C99 designated initializers, or at least split over multiple lines. Or a plain array of of allowed transitions, (current, target) -> bool. Or a function with switch-case statement.
I moved to designated initializers which I think made quite the improvement.
This code was at first in one function per barrier, with the states encoded in
if/then/else, later it was merged into a single function. The logic was
however getting so convoluted that several subtle bugs were found there, after
moving to this configuration-driven approach it has been more reliable and (IMHO)
easier to review.
>> /*
>> * If the postmaster crashed we cannot end up with a processed database so
>> * we have no alternative other than exiting. When enabling checksums we
>> * won't at this time have changed the pg_control version to enabled so
>> * when the cluster comes back up processing will have to be restarted.
>> * When disabling, the pg_control version will be set to off before this
>> * so when the cluster comes up checksums will be off as expected.
>> */
>> if (status == BGWH_POSTMASTER_DIED)
>> ereport(FATAL,
>> errcode(ERRCODE_ADMIN_SHUTDOWN),
>> errmsg("cannot enable data checksums without the postmaster process"),
>> errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
>
> Do we reach here when disabling checksums? The comment suggests so, but in that case the hint would be wrong.
The comment is wrong, we only process databases when enabling checksums so
during disabling we cannot reach here. Fixed.
>> /*
>> * launcher_exit
>> *
>> * Internal routine for cleaning up state when the launcher process exits. We
>> * need to clean up the abort flag to ensure that processing started again if
>> * it was previously aborted (note: started again, *not* restarted from where
>> * it left off).
>> */
>> static void
>> launcher_exit(int code, Datum arg)
>> {
>> if (launcher_running)
>> {
>> LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
>> launcher_running = false;
>> DataChecksumsWorkerShmem->launcher_running = false;
>> /*
>> * TODO: how to really handle the worker still running when the
>> * launcher exits?
>> */
>> if (DataChecksumsWorkerShmem->worker_running)
>> ereport(LOG,
>> errmsg("data checksums launcher exiting while worker is still running"));
>> LWLockRelease(DataChecksumsWorkerLock);
>> }
>> }
>
> What "abort flag"?
There is an abort_requested flag which is set by the signal handler, but I'm
not sure why the clearing of the flag wasn't done here as the comment states it
should be. Fixed.
>> /*
>> * Get a list of all databases to process. This may include databases that
>> * were created during our runtime. Since a database can be created as a
>> * copy of any other database (which may not have existed in our last
>> * run), we have to repeat this loop until no new databases show up in the
>> * list. Here the initial list for the loop processing is generated after
>> * waiting for all existing transactions to finish to ensure that we can
>> * see any database which was created even if the transaction in which it
>> * was created started before checksums were being processed.
>> */
>> WaitForAllTransactionsToFinish();
>> DatabaseList = BuildDatabaseList();
>
> What if a database is dropped and another one is created with the same database OID?
Good point. We have the name of the database so we could compare Oid and name,
and require both to match. That would still be foiled by a new database using
the same Oid and name though. Any ideas on what could be done?
>> /*
>> * This is the only place where we check if we are asked to abort, the
>> * abortion will bubble up from here. It's safe to check this without
>> * a lock, because if we miss it being set, we will try again soon.
>> */
>> Assert(operation == ENABLE_DATACHECKSUMS);
>> LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
>> if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
>> abort_requested = true;
>> LWLockRelease(DataChecksumsWorkerLock);
>
> The comment justifies that it's safe to do this without a lock, but it grabs the lock anyway.
I have a feeling that's an old comment that was missed when updating, we should
grab a lock here. Fixed.
>> @@ -107,7 +107,8 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
>> */
>> if (!PageIsNew(page))
>> {
>> - if (DataChecksumsEnabled())
>> + HOLD_INTERRUPTS();
>> + if (DataChecksumsNeedVerify())
>> {
>> checksum = pg_checksum_page(page, blkno);
>> @@ -118,6 +119,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
>> *checksum_failure_p = true;
>> }
>> }
>> + RESUME_INTERRUPTS();
>> /*
>> * The following checks don't prove the header is correct, only that
>
> This is to ensure that we don't absorb a barrier while calculating the checksum, which could transition us from "on" to "off" ? This is probably not really needed because there are no CHECK_FOR_INTERRUPTS() calls in pg_checksum_page(), but better safe than sorry I guess. Another approach would be to check DataChecksumsNeedVerify() again if the checksum didn't match. In any case, a comment would be in order.
Right, it's a belts-and-suspenders type of safety measure. Comment added.
> s/datachecksumsworker.c/datachecksum_state.c/
Fixed
> DataChecksumsOnInProgress() and DataChecksumsOffInProgress() are unused.
Fixed
> Attached are a few more trivial fixes, in patch format.
Thanks, applied.
The above changes are kept in 0003 in order to make them easier to see in this
somewhat large patchset.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260317-0001-Add-proper-WAL-record-for-XLOG_CHECKPOINT_.patch (3.9K, ../../[email protected]/2-v20260317-0001-Add-proper-WAL-record-for-XLOG_CHECKPOINT_.patch)
download | inline diff:
From f584ec8fb06fdb12c1c1a0d51780cb1769e83d18 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 12 Mar 2026 18:33:02 +0100
Subject: [PATCH v20260317 1/3] Add proper WAL record for XLOG_CHECKPOINT_REDO
XLOG_CHECKPOINT_REDO only contains the wal_level copied straight in
without an encapsulating record structure. While it works, it makes
future uses of XLOG_CHECKPOINT_REDO hard as there is nowhere to put
new data items. This fix this was inspired by the online checksums
patch which adds data to this record, but this change has value on
its own.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/..
---
src/backend/access/rmgrdesc/xlogdesc.c | 6 +++---
src/backend/access/transam/xlog.c | 8 +++++++-
src/backend/postmaster/walsummarizer.c | 5 ++++-
src/include/access/xlog_internal.h | 6 ++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 9044b952117..44194d3ea17 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -163,10 +163,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- int wal_level;
+ xl_checkpoint_redo xlrec;
- memcpy(&wal_level, rec, sizeof(int));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(wal_level));
+ memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
+ appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f5c9a34374d..2c1c6f88b74 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7179,9 +7179,15 @@ CreateCheckPoint(int flags)
*/
if (!shutdown)
{
+ xl_checkpoint_redo redo_rec;
+
+ WALInsertLockAcquire();
+ redo_rec.wal_level = wal_level;
+ WALInsertLockRelease();
+
/* Include WAL level in record for WAL summarizer's benefit. */
XLogBeginInsert();
- XLogRegisterData(&wal_level, sizeof(wal_level));
+ XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
/*
diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c
index e1aa102f41d..bdc2236a8c1 100644
--- a/src/backend/postmaster/walsummarizer.c
+++ b/src/backend/postmaster/walsummarizer.c
@@ -1428,8 +1428,11 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward)
if (info == XLOG_CHECKPOINT_REDO)
{
+ xl_checkpoint_redo xlrec;
+
/* Payload is wal_level at the time record was written. */
- memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int));
+ memcpy(&xlrec, XLogRecGetData(xlogreader), sizeof(xl_checkpoint_redo));
+ record_wal_level = xlrec.wal_level;
}
else if (info == XLOG_CHECKPOINT_SHUTDOWN)
{
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 629ac3a7d3e..faabba73ef2 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -303,6 +303,12 @@ typedef struct xl_end_of_recovery
int wal_level;
} xl_end_of_recovery;
+/* checkpoint redo */
+typedef struct xl_checkpoint_redo
+{
+ int wal_level;
+} xl_checkpoint_redo;
+
/*
* The functions in xloginsert.c construct a chain of XLogRecData structs
* to represent the final WAL record.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 52f8603a7be..3e6a0aae828 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4379,6 +4379,7 @@ xl_btree_split
xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
+xl_checkpoint_redo
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
[application/octet-stream] v20260317-0002-Online-enabling-and-disabling-of-data-chec.patch (232.6K, ../../[email protected]/3-v20260317-0002-Online-enabling-and-disabling-of-data-chec.patch)
download | inline diff:
From 83c4b97112c7a84d25ca7710cabf6b2fafabf20b Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 15 Aug 2025 14:48:02 +0200
Subject: [PATCH v20260317 2/3] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calcuated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests for online processing are gated begind the
PG_TEST_EXTRA flag to some degree due to being very time consuming
to run.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 77 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 12 +
doc/src/sgml/wal.sgml | 127 +-
src/backend/access/heap/heapam.c | 3 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 624 +++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 9 +
src/backend/postmaster/datachecksum_state.c | 1715 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 13 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 4 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 1 +
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/pg_control.h | 8 +-
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/proc.h | 2 +-
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 81 +
.../test_checksums/t/006_pgbench_single.pl | 263 +++
.../test_checksums/t/007_pgbench_standby.pl | 385 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 172 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 28 +
.../modules/test_checksums/test_checksums.c | 222 +++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 8 +
78 files changed, 5333 insertions(+), 60 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 8cdd826fbd3..4c083335d89 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..46635d27e94 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,81 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums mode can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums mode to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all data checksums
+ have been calculated, and written, for all pages the cluster will
+ automatically set data checksums mode to <literal>on</literal>.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum mode to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum mode will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index e2db5bcc78c..d9378af0606 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index fd55b9ad23f..e805487344d 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 462019a972c..df7e8d2b2ea 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3781,9 +3781,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3792,8 +3797,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7524,6 +7529,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index e9e393495df..e764b8be04d 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 43f208df272..b18ea9b5818 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject delays
+ and re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..df41fb54cb6 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases or tables.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,124 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will put the cluster checksum mode in
+ <literal>inprogress-on</literal> mode. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum mode will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in the application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> mode,
+ for any reason, then the checksum enable process must be restarted manually.
+ To do this, re-execute the function <function>pg_enable_data_checksums()</function>
+ once the cluster has been restarted. The process will start over, there is
+ no support for resuming work from where it was interrupted. If the cluster
+ is stopped while in <literal>inprogress-off</literal>, then the checksum
+ state will be set to <literal>off</literal> when the cluster is
+ restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum mode to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the mode will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing. There is no way to restart from where
+ processing was interrupted.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Rewriting all pages is not needed when disabling data checksums, but
+ checkpoints are still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index e5bd062de77..317dab3a3d2 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -6031,8 +6031,7 @@ l4:
&new_xmax, &new_infomask, &new_infomask2);
if (PageIsAllVisible(BufferGetPage(buf)) &&
- visibilitymap_clear(rel, block, vmbuffer,
- VISIBILITYMAP_ALL_FROZEN))
+ visibilitymap_clear(rel, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN))
cleared_all_frozen = true;
START_CRIT_SECTION();
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..2986a1c9996 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+static const char *
+get_checksum_version_string(ChecksumType checksum)
+{
+ switch (checksum)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_version_string(xlrec.new_checksumtype));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_version_string(checkpoint->dataChecksumVersion),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_version_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..389a2054cd6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -92,6 +92,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +554,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +654,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumType LocalDataChecksumVersion = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumVersion.
+ * See SetLocalDataChecksumVersion().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +736,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -831,9 +852,10 @@ XLogInsertRecord(XLogRecData *rdata,
* only happen just after a checkpoint, so it's better to be slow in
* this case and fast otherwise.
*
- * Also check to see if fullPageWrites was just turned on or there's a
- * running backup (which forces full-page writes); if we weren't
- * already doing full-page writes then go back and recompute.
+ * Also check to see if fullPageWrites was just turned on, there's a
+ * running backup or if checksums are enabled (all of which forces
+ * full-page writes); if we weren't already doing full-page writes
+ * then go back and recompute.
*
* If we aren't doing full-page writes then RedoRecPtr doesn't
* actually affect the contents of the XLOG record, so we'll update
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,362 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true iff data checksums are enabled or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksumsworker.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION ||
+ LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksumsworker.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
+ */
+bool
+DataChecksumsNeedVerify(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * DataChecksumsOnInProgress
+ * Returns whether data checksums are being enabled
+ *
+ * Most operations don't need to worry about the "inprogress" states, and
+ * should use DataChecksumsNeedVerify() or DataChecksumsNeedWrite(). The
+ * "inprogress-on" state for enabling checksums is used when the checksum
+ * worker is setting checksums on all pages, it can thus be used to check for
+ * aborted checksum processing which need to be restarted.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsOnInProgress(void)
{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+}
+
+/*
+ * DataChecksumsOffInProgress
+ * Returns whether data checksums are being disabled
+ *
+ * The "inprogress-off" state for disabling checksums is used for when the
+ * worker resets the catalog state. DataChecksumsNeedVerify() or
+ * DataChecksumsNeedWrite() should be used for deciding whether to read/write
+ * checksums.
+ */
+bool
+DataChecksumsOffInProgress(void)
+{
+ return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Enables data checksums cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksumsworker.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified. See datachecksumsworker.c for a longer discussion on
+ * how data checksums can be enabled in a running cluster.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition of "on" in all backends. When done we know that
+ * data checksums are enabled in all backends and data checksums are both
+ * written and verified.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalControlData
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumVersion(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumVersion(uint32 data_checksum_version)
+{
+ LocalDataChecksumVersion = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ switch (LocalDataChecksumVersion)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ /*
+ * Since all states are covered in the switch statement, this cannot
+ * happen.
+ */
+ Assert(false);
+ return "?";
}
/*
@@ -4925,6 +5298,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumVersion(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5468,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5547,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumVersion = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6624,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("data checksums state has been set to off"),
+ errhint("If checksums were being enabled during shutdown then processing must be manually restarted."));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6970,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7548,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7610,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7673,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7826,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7972,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8319,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumVersion;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8761,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksumtype = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8905,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+ SetLocalDataChecksumVersion(checkPoint.dataChecksumVersion);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8969,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8996,9 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
+ uint64 barrier;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +9037,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9050,42 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * XXX: I'm not sure this is right, and if it is this needs
+ * refactoring to be pretty (as well as in the _REDO case
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+ if (checkPoint.dataChecksumVersion != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ {
+ switch (checkPoint.dataChecksumVersion)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9247,43 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+ uint64 barrier;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ {
+ switch (redo_rec.data_checksum_version)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9336,58 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+ uint64 barrier;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ /*
+ * XXX Could this end up written to the control file prematurely? IIRC
+ * that happens during checkpoint, so what if that gets triggered e.g.
+ * because someone runs CHECKPOINT? If we then crash (or something
+ * like that), could that confuse the instance?
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksumtype;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ switch (state.new_checksumtype)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..cdbaa0c0337 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point were we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f1ed7b58f13..c500e41e2c5 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1442,6 +1442,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..9803ece0792 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumVersion();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 0104a86b9ec..26a4972b84e 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -143,6 +144,14 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
}
};
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..2384be7f657
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1715 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a database at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads wont fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of databases which exist at the
+ * start of checksumming, and once all are processed will regenerate the list
+ * and start over processing any new entries. Once there are no new entries on
+ * the list, processing will end. All databases MUST BE successfully processed
+ * in order for data checksums to be enabled, the only exception are databases
+ * which are dropped before having been processed.
+
+ * Any new relation in a processed database, created during processing, will
+ * see the in-progress state and will automatically be checksummed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer verified. This ensure
+ * that backends which have yet to move from the "on" state will still be able
+ * to process data checksum validation.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling, or disabling, data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers, before
+ * updating the controlfile with the state all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one set being empty:
+ *
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set of "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the set of conditions to use is
+ * looked up in the checksum_barriers struct. The struct member for the target
+ * state defines which state the backend must currently be in, and which it
+ * must not be in.
+ *
+ * The reason for this explicit checking is to ensure that processing cannot
+ * be started such that it breaks the assumptions of the state machine. See
+ * datachecksumsworker.c for a lengthy discussion on these states.
+ *
+ * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
+ * barrier_ne in the below checksum_barriers definition.
+ */
+#define MAX_BARRIER_CONDITIONS 2
+typedef struct ChecksumBarrierCondition
+{
+ /* The target state of the barrier */
+ int target;
+ /* A set of states in which at least one MUST match the current state */
+ int barrier_eq[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_eq set */
+ int barrier_eq_sz;
+ /* A set of states which all MUST NOT match the current state */
+ int barrier_ne[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_ne set */
+ int barrier_ne_sz;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[4] =
+{
+ {PG_DATA_CHECKSUM_OFF, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION}, 2, {PG_DATA_CHECKSUM_VERSION}, 1},
+ {PG_DATA_CHECKSUM_VERSION, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION}, 1, {0}, 0},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, {0}, 0, {PG_DATA_CHECKSUM_VERSION}, 1},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, {PG_DATA_CHECKSUM_VERSION}, 1, {0}, 0},
+};
+
+/*
+ * Number of times we retry to open a database before giving up and consider
+ * it to have failed processing.
+ */
+#define DATACHECKSUMSWORKER_MAX_DB_RETRIES 5
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsWorkerShmemStruct
+{
+ /*
+ * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * If a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ bool worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsWorkerShmemStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+typedef struct DataChecksumsWorkerResultEntry
+{
+ Oid dboid;
+ DataChecksumsWorkerResult result;
+ int retries;
+} DataChecksumsWorkerResultEntry;
+
+
+/*
+ * Flag set by the interrupt handler
+ */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/*
+ * Are we enabling data checksums, or disabling them?
+ */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ *
+ */
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ const ChecksumBarrierCondition *condition = NULL;
+ int target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumVersion(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers); i++)
+ {
+ if (checksum_barriers[i].target == target_state)
+ condition = &checksum_barriers[i];
+ }
+ Assert(condition);
+
+ /*
+ * The current state MUST be equal to one of the EQ states defined in this
+ * barrier condition. If the EQ states array is zero then that implies
+ * that the current state can match any state, so fastpath check for that
+ * first.
+ */
+ if (condition->barrier_eq_sz == 0)
+ found = true;
+ else
+ {
+ for (int i = 0; i < condition->barrier_eq_sz; i++)
+ {
+ if (current == condition->barrier_eq[i])
+ found = true;
+ }
+ }
+
+ /*
+ * The current state MUST NOT be equal to any of the NE states defined in
+ * this barrier condition.
+ */
+ for (int i = 0; i < condition->barrier_ne_sz; i++)
+ {
+ if (current == condition->barrier_ne[i])
+ found = false;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumVersion(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ *
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumsWorkerShmem->launch_operation = op;
+ DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
+ DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumsWorkerShmem->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ if (!relns)
+ return false;
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %dblocks)",
+ relns, RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock before we can flag as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the master happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here. It's safe to check this without
+ * a lock, because if we miss it being set, we will try again soon.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /*
+ * As of now we only update the block counter for main forks in order
+ * to not cause too frequent calls. TODO: investigate whether we
+ * should do it more frequent?
+ */
+ if (forkNum == MAIN_FORKNUM)
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ pfree(relns);
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+ elog(DEBUG2,
+ "data checksum processing done for relation with OID %u: %s",
+ relationId, (aborted ? "aborted" : "finished"));
+
+ CommitTransactionCommand();
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, make sure we retry processing
+ * this database. This will make the datachecksumsworker move on to the
+ * next database and quite likely fail with the same problem. TODO: Maybe
+ * we need a backoff to avoid running through all the databases here in
+ * short order.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("failed to start worker for enabling data checksums in database \"%s\", retrying",
+ db->dbname),
+ errhint("The max_worker_processes setting might be too low."));
+ return DATACHECKSUMSWORKER_RETRYDB;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the pg_control version to enabled so
+ * when the cluster comes back up processing will have to be restarted.
+ * When disabling, the pg_control version will be set to off before this
+ * so when the cluster comes up checksums will be off as expected.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ DataChecksumsWorkerShmem->worker_running = true;
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ DataChecksumsWorkerShmem->worker_running = false;
+
+ return DataChecksumsWorkerShmem->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+
+ /*
+ * TODO: how to really handle the worker still running when the
+ * launcher exits?
+ */
+ if (DataChecksumsWorkerShmem->worker_running)
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running"));
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumsWorkerShmem->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /*
+ * Initialize a connection to shared catalogs only.
+ */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->launcher_running = true;
+ DataChecksumsWorkerShmem->operation = operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ }
+ else
+ {
+ Assert(false);
+ }
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will repeatedly generate a list of databases to process for enabling
+ * checksums. Until no new databases are found, this will loop around computing
+ * a new list and comparing it to the already seen ones.
+ *
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ HTAB *ProcessedDatabases = NULL;
+ HASHCTL hash_ctl;
+ bool found_failed = false;
+ int cumulative_total = 0;
+
+ /* Initialize a hash tracking all processed databases */
+ memset(&hash_ctl, 0, sizeof(hash_ctl));
+ hash_ctl.keysize = sizeof(Oid);
+ hash_ctl.entrysize = sizeof(DataChecksumsWorkerResultEntry);
+ ProcessedDatabases = hash_create("Processed databases",
+ 64,
+ &hash_ctl,
+ HASH_ELEM | HASH_BLOBS);
+
+ /*
+ * Set up so first run processes shared catalogs, but not once in every
+ * db.
+ */
+ DataChecksumsWorkerShmem->process_shared_catalogs = true;
+
+ /*
+ * Get a list of all databases to process. This may include databases that
+ * were created during our runtime. Since a database can be created as a
+ * copy of any other database (which may not have existed in our last
+ * run), we have to repeat this loop until no new databases show up in the
+ * list. Here the initial list for the loop processing is generated after
+ * waiting for all existing transactions to finish to ensure that we can
+ * see any database which was created even if the transaction in which it
+ * was created started before checksums were being processed.
+ */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /* Allow a test case to modify the initial list of databases */
+ INJECTION_POINT("datachecksumsworker-initial-dblist", &DatabaseList);
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ while (true)
+ {
+ int processed_databases = 0;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+ DataChecksumsWorkerResultEntry *entry;
+ bool found;
+
+ /*
+ * Check if this database has been processed already, and if so
+ * whether it should be retried or skipped.
+ */
+ entry = (DataChecksumsWorkerResultEntry *) hash_search(ProcessedDatabases, &db->dboid,
+ HASH_FIND, NULL);
+
+ if (entry)
+ {
+ if (entry->result == DATACHECKSUMSWORKER_RETRYDB)
+ {
+ /*
+ * Limit the number of retries to avoid infinite looping
+ * in case there simply won't be enough workers in the
+ * cluster to finish this operation.
+ */
+ if (entry->retries > DATACHECKSUMSWORKER_MAX_DB_RETRIES)
+ entry->result = DATACHECKSUMSWORKER_FAILED;
+ }
+
+ /* Skip if this database has been processed already */
+ if (entry->result != DATACHECKSUMSWORKER_RETRYDB)
+ continue;
+ }
+
+ result = ProcessDatabase(db);
+ processed_databases++;
+ cumulative_total++;
+
+ /*
+ * Update the number of processed databases in the progress
+ * report.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ cumulative_total);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-fail-db", &result);
+
+ if (result == DATACHECKSUMSWORKER_SUCCESSFUL)
+ {
+ /*
+ * If one database has completed shared catalogs, we don't
+ * have to process them again.
+ */
+ if (DataChecksumsWorkerShmem->process_shared_catalogs)
+ DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ entry = hash_search(ProcessedDatabases, &db->dboid, HASH_ENTER, &found);
+ entry->dboid = db->dboid;
+ entry->result = result;
+ if (!found)
+ entry->retries = 0;
+ else
+ entry->retries++;
+ }
+
+ elog(DEBUG1,
+ "%i databases processed for data checksum enabling (%i this iteration), %s",
+ cumulative_total, processed_databases,
+ (processed_databases ? "process with restart" : "process completed"));
+
+ FreeDatabaseList(DatabaseList);
+
+ /*
+ * If no databases were processed in this run of the loop, we have now
+ * finished all databases and no concurrently created ones can exist.
+ */
+ if (processed_databases == 0)
+ break;
+
+ /*
+ * Re-generate the list of databases for another pass in case any new
+ * databases were created while we were running. Since the initial
+ * list was generated after waiting for all transaction to finish we
+ * know that all new databases found here must have been created while
+ * seeing the new checksum state. By waiting for all transactions
+ * here as well we know that any database created using an existing db
+ * as a template (which may have been used before it had checksums
+ * enabled) will be committed.
+ */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+ }
+
+ /*
+ * ProcessedDatabases now has all databases and the results of their
+ * processing. Failure to enable checksums for a database can be because
+ * they actually failed for some reason, or because the database was
+ * dropped between us getting the database list and trying to process it.
+ * Get a fresh list of databases to detect the second case where the
+ * database was dropped before we had started processing it. If a database
+ * still exists, but enabling checksums failed then we fail the entire
+ * checksumming process and exit with an error.
+ */
+ DatabaseList = BuildDatabaseList();
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResultEntry *entry;
+ bool found;
+
+ entry = hash_search(ProcessedDatabases, (void *) &db->dboid,
+ HASH_FIND, &found);
+
+ /*
+ * We are only interested in the processed databases which failed, and
+ * where the failed database still exists. This indicates that
+ * enabling checksums actually failed, and not that the failure was
+ * due to the db being concurrently dropped.
+ */
+ if (found && entry->result == DATACHECKSUMSWORKER_FAILED)
+ {
+ ereport(WARNING,
+ errmsg("failed to enable data checksums in \"%s\"", db->dbname));
+ found_failed = found;
+ continue;
+ }
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ if (found_failed)
+ {
+ /* Disable checksums on cluster, because we failed */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumsWorkerShmemSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsWorkerShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumsWorkerShmemInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsWorkerShmemInit(void)
+{
+ bool found;
+
+ DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsWorkerShmemSize(),
+ &found);
+
+ if (!found)
+ MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /*
+ * Only include temporary relations when asked for a temp relation
+ * list.
+ */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
+ VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumsWorkerShmem->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3fac46c402b..c898c53b623 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2999,6 +2999,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add checksumming processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 21f03864a66..4674c0758c1 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index a4785daf1e5..bbd59d8af44 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsWorkerShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -316,6 +318,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsWorkerShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index de85911e3ac..8da8f8fecf1 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,8 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +119,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -1511,7 +1513,7 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
static char *pageCopy = NULL;
/* If we don't need a checksum, just return the passed-in data */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
return page;
/*
@@ -1540,9 +1542,14 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
void
PageSetChecksumInplace(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index f2f8d3ff75f..07fb6bacb40 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..8105a0af79b 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,9 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_FINISHCONDITION "Waiting for data checksums to be enabled."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -366,6 +369,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f907335990..e20b02c03da 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 26118661f07..edd429e83cb 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumVersion();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index a5a0edf2534..fbd415a1b8b 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -531,11 +531,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38aaf82f120..661b8757979 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -628,7 +636,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e686d88afc4..0a258e76b83 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -540,11 +540,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..47afa82d9c5 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumVersion);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 5db5d20136f..33ea514a52d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -53,6 +53,7 @@ command_fails_like(
# case exercising the new rmgr below.
command_like(
[ 'pg_waldump', '--rmgr=list' ], qr/^XLOG
+XLOG2
Transaction
Storage
CLOG
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..7cec6301769 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -26,6 +26,7 @@
/* symbol name, textual name, redo, desc, identify, startup, cleanup, mask, decode */
PG_RMGR(RM_XLOG_ID, "XLOG", xlog_redo, xlog_desc, xlog_identify, NULL, NULL, NULL, xlog_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
PG_RMGR(RM_XACT_ID, "Transaction", xact_redo, xact_desc, xact_identify, NULL, NULL, NULL, xact_decode)
PG_RMGR(RM_SMGR_ID, "Storage", smgr_redo, smgr_desc, smgr_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_CLOG_ID, "CLOG", clog_redo, clog_desc, clog_identify, NULL, NULL, NULL, NULL)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..256f3499481 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsOnInProgress(void);
+extern bool DataChecksumsOffInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern void InitLocalDataChecksumVersion(void);
+extern void SetLocalDataChecksumVersion(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index faabba73ef2..8fb5b978f51 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumType new_checksumtype;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..5ac74d7ef1c 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumVersion;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
@@ -223,7 +229,7 @@ typedef struct ControlFileData
bool float8ByVal; /* float8, int8, etc pass-by-value? */
/* Are data pages protected by checksums? Zero if no checksum version */
- uint32 data_checksum_version;
+ uint32 data_checksum_version; /* persistent */
/*
* True if the default signedness of char is "signed" on a platform where
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fc8d82665b8..634a78af6ac 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12534,6 +12534,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 's', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..fb4fca24991 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..5601241a803
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsWorkerShmemSize(void);
+extern void DataChecksumsWorkerShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+ /* TODO: VERIFY_DATACHECKSUMS, */
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_RETRYDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index 3de58ba4312..ed7e0abcbfa 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..b6bbba82949 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum version 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_VERSION defines that data checksums are enabled in the
+ * cluster and PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF}_VERSION defines that data
+ * checksums are either currently being enabled or disabled.
+ */
+typedef enum ChecksumType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION
+} ChecksumType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index e94ebce95b9..0c02f2ae38f 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index bf3094f0f7d..e1f8320cdf3 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -524,7 +524,7 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs;
* Background writer, checkpointer, WAL writer, WAL summarizer, and archiver
* run during normal operation. Startup process and WAL receiver also consume
* 2 slots, but WAL writer is launched only after startup has exited, so we
- * only need 6 slots.
+ * only need 6 slots to cover these.
*/
#define MAX_IO_WORKERS 32
#define NUM_AUXILIARY_PROCS (6 + MAX_IO_WORKERS)
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index a1540269cf5..ea3c7499f6f 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 7c052803c98..f813c33ed22 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..397f6411cb5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 'on');
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..a4d35e9f95c
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksumming to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..c5732a2c7da
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby_1->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby_1->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby_1, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby_1, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby_1->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby_1->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby_1, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby_1, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby_1->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f33e278de28
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksumming to block on, in this case a pre-existing
+# temporary table which is kept open while processing is started. We can
+# accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..5f7f4102b9e
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+# Force the enable checksums processing to make multiple passes by removing
+# one database from the list in the first pass. This will simulate a CREATE
+# DATABASE during processing. Doing this via fault injection makes the test
+# not be subject to exact timing.
+$node->safe_psql('postgres', 'SELECT dcw_prune_dblist(true);');
+enable_data_checksums($node, wait => 'on');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..727c804bbd5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,263 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test is very expensive to run, so by default it runs a pared-down test
+# suite. Setting $extended to 1 will run the full suite and can be done by
+# setting PG_TEST_EXTRA to "checksum_extended".
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+$node->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..8e099fdbe65
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,385 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby_1;
+my $node_standby_1_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+
+ # Terminate any currently running pgbench process before continuing
+ $pgbench_primary->finish if $pgbench_primary;
+
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ my $clients = 1;
+ my $runtime = 10;
+
+ if ($extended)
+ {
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench_primary = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby_1, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby_1->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby_1->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby_1, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby_1, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby_1, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby_1->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby_1->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby_1->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby_1->is_alive)
+ {
+ $node_standby_1->start;
+ $node_standby_1->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby_1->append_conf('postgresql.conf',
+ qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby_1->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby_1->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby_1, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently)
+ # XXX should maybe try stopping nodes in the opposite order too?
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby_1->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby_1->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_1_loglocation = -s $node_standby_1->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby_1->is_alive)
+{
+ $node_standby_1->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby_1, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+$node_standby_1->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..6c98e0ab23d
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,172 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/)
+{
+ plan skip_all => 'Extended checksum tests not enabled';
+}
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ my $clients = 1 + int(rand(15));
+
+ my @cmd = ('pgbench', '-p', $port, '-T', '600', '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if (cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('node_pitr');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..778c696abc6
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 'off');
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..fb1c802461b
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,28 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_prune_dblist(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..03752adff65
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,222 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_dblist(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+dc_fail_database(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+
+ if (first_pass)
+ *res = DATACHECKSUMSWORKER_FAILED;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fail-db",
+ "test_checksums",
+ "dc_fail_database",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fail-db");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to remove an entry from the Databaselist to force re-processing since
+ * not all databases could be processed in the first iteration of the loop.
+ */
+void
+dc_dblist(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ List **DatabaseList = (List **) arg;
+
+ if (first_pass)
+ *DatabaseList = list_delete_last(*DatabaseList);
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_prune_dblist);
+Datum
+dcw_prune_dblist(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-initial-dblist",
+ "test_checksums",
+ "dc_dblist",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-initial-dblist");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..8b3c4af3284 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3863,6 +3863,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 32bea58db2c..24e68b76f3e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2079,6 +2079,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index b99462bf946..f25394545df 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3e6a0aae828..c2f9a2a9b9a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumType
Chromosome
CkptSortItem
CkptTsStatus
@@ -602,6 +604,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -620,6 +623,10 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsWorkerResultEntry
+DataChecksumsWorkerShmemStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4380,6 +4387,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
[application/octet-stream] v20260317-0003-review_comments.patch (41.3K, ../../[email protected]/4-v20260317-0003-review_comments.patch)
download | inline diff:
From 91106a6cbfc010416c97d040daa87f125c24e24b Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 17 Mar 2026 22:16:06 +0100
Subject: [PATCH v20260317 3/3] review_comments
---
src/backend/access/rmgrdesc/xlogdesc.c | 10 +-
src/backend/access/transam/xlog.c | 144 +++++++-----------
src/backend/postmaster/auxprocess.c | 2 +-
src/backend/postmaster/datachecksum_state.c | 108 ++++++++-----
src/backend/postmaster/postmaster.c | 2 +-
src/backend/storage/page/bufpage.c | 7 +
src/backend/utils/init/postinit.c | 2 +-
src/backend/utils/misc/guc_tables.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +-
src/include/access/rmgrlist.h | 2 +-
src/include/access/xlog.h | 6 +-
src/include/access/xlog_internal.h | 2 +-
src/include/catalog/pg_control.h | 4 +-
src/include/storage/checksum.h | 16 +-
.../modules/test_checksums/t/002_restarts.pl | 2 +-
.../modules/test_checksums/t/004_offline.pl | 6 +-
src/tools/pgindent/typedefs.list | 2 +-
17 files changed, 165 insertions(+), 156 deletions(-)
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 2986a1c9996..47c3c8363ed 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -56,15 +56,15 @@ get_wal_level_string(int wal_level)
}
static const char *
-get_checksum_version_string(ChecksumType checksum)
+get_checksum_version_string(ChecksumStateType checksum)
{
switch (checksum)
{
case PG_DATA_CHECKSUM_VERSION:
return "on";
- case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
return "inprogress-off";
- case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
return "inprogress-on";
case PG_DATA_CHECKSUM_OFF:
return "off";
@@ -85,7 +85,7 @@ xlog2_desc(StringInfo buf, XLogReaderState *record)
xl_checksum_state xlrec;
memcpy(&xlrec, rec, sizeof(xl_checksum_state));
- appendStringInfoString(buf, get_checksum_version_string(xlrec.new_checksumtype));
+ appendStringInfoString(buf, get_checksum_version_string(xlrec.new_checksum_state));
}
}
@@ -124,7 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
- get_checksum_version_string(checkpoint->dataChecksumVersion),
+ get_checksum_version_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 389a2054cd6..883904d390f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -661,11 +661,11 @@ static bool updateMinRecoveryPoint = true;
* avoid locking for interrogating the data checksum state. Possible values
* are the data checksum versions defined in storage/checksum.h.
*/
-static ChecksumType LocalDataChecksumVersion = 0;
+static ChecksumStateType LocalDataChecksumState = 0;
/*
- * Variable backing the GUC, keep it in sync with LocalDataChecksumVersion.
- * See SetLocalDataChecksumVersion().
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
*/
int data_checksums = 0;
@@ -853,9 +853,8 @@ XLogInsertRecord(XLogRecData *rdata,
* this case and fast otherwise.
*
* Also check to see if fullPageWrites was just turned on, there's a
- * running backup or if checksums are enabled (all of which forces
- * full-page writes); if we weren't already doing full-page writes
- * then go back and recompute.
+ * running backup (which forces full-page writes); if we weren't
+ * already doing full-page writes then go back and recompute.
*
* If we aren't doing full-page writes then RedoRecPtr doesn't
* actually affect the contents of the XLOG record, so we'll update
@@ -4654,7 +4653,7 @@ GetMockAuthenticationNonce(void)
*
* Returns true iff data checksums are enabled or are in the process of being
* enabled. During "inprogress-on" and "inprogress-off" states checksums must
- * be written even though they are not verified (see datachecksumsworker.c for
+ * be written even though they are not verified (see datachecksum_state.c for
* a longer discussion).
*
* This function is intended for callsites which are about to write a data page
@@ -4665,9 +4664,9 @@ GetMockAuthenticationNonce(void)
bool
DataChecksumsNeedWrite(void)
{
- return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION ||
- LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION ||
- LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
}
/*
@@ -4676,7 +4675,7 @@ DataChecksumsNeedWrite(void)
*
* Data checksums are only verified if they are fully enabled in the cluster.
* During the "inprogress-on" and "inprogress-off" states they are only
- * updated, not verified (see datachecksumsworker.c for a longer discussion).
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
*
* This function is intended for callsites which have read data and are about
* to perform checksum validation based on the result of this. Calling this
@@ -4687,38 +4686,7 @@ DataChecksumsNeedWrite(void)
bool
DataChecksumsNeedVerify(void)
{
- return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION);
-}
-
-/*
- * DataChecksumsOnInProgress
- * Returns whether data checksums are being enabled
- *
- * Most operations don't need to worry about the "inprogress" states, and
- * should use DataChecksumsNeedVerify() or DataChecksumsNeedWrite(). The
- * "inprogress-on" state for enabling checksums is used when the checksum
- * worker is setting checksums on all pages, it can thus be used to check for
- * aborted checksum processing which need to be restarted.
- */
-bool
-DataChecksumsOnInProgress(void)
-{
- return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
-}
-
-/*
- * DataChecksumsOffInProgress
- * Returns whether data checksums are being disabled
- *
- * The "inprogress-off" state for disabling checksums is used for when the
- * worker resets the catalog state. DataChecksumsNeedVerify() or
- * DataChecksumsNeedWrite() should be used for deciding whether to read/write
- * checksums.
- */
-bool
-DataChecksumsOffInProgress(void)
-{
- return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
}
/*
@@ -4745,10 +4713,10 @@ SetDataChecksumsOnInProgress(void)
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
- XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
SpinLockRelease(&XLogCtl->info_lck);
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
@@ -4761,7 +4729,7 @@ SetDataChecksumsOnInProgress(void)
* shutdown while waiting we want to come back up with checksums enabled.
*/
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
- ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -4781,7 +4749,7 @@ SetDataChecksumsOnInProgress(void)
* set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
* and the second one to set the state to "on" (done here). Below is a short
* description of the processing, a more detailed write-up can be found in
- * datachecksumsworker.c.
+ * datachecksum_state.c.
*
* To start the process of enabling data checksums in a running cluster the
* data_checksum_version state must be changed to "inprogress-on". This state
@@ -4790,7 +4758,7 @@ SetDataChecksumsOnInProgress(void)
* validation during the process. When all existing pages are guaranteed to
* have checksums, and all new pages will be initiated with checksums, the
* state can be changed to "on". Once the state is "on" checksums will be both
- * written and verified. See datachecksumsworker.c for a longer discussion on
+ * written and verified. See datachecksum_state.c for a longer discussion on
* how data checksums can be enabled in a running cluster.
*
* This function blocks until all backends in the cluster have acknowledged the
@@ -4809,7 +4777,7 @@ SetDataChecksumsOn(void)
* The only allowed state transition to "on" is from "inprogress-on" since
* that state ensures that all pages will have data checksums written.
*/
- if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
{
SpinLockRelease(&XLogCtl->info_lck);
elog(PANIC, "checksums not in \"inprogress-on\" mode");
@@ -4894,10 +4862,10 @@ SetDataChecksumsOff(void)
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
- XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
@@ -4960,7 +4928,7 @@ SetDataChecksumsOff(void)
}
/*
- * InitLocalControlData
+ * InitLocalDataChecksumState
*
* Set up backend local caches of controldata variables which may change at
* any point during runtime and thus require special cased locking. So far
@@ -4968,17 +4936,17 @@ SetDataChecksumsOff(void)
* purpose enough to handle future cases.
*/
void
-InitLocalDataChecksumVersion(void)
+InitLocalDataChecksumState(void)
{
SpinLockAcquire(&XLogCtl->info_lck);
- SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
}
void
-SetLocalDataChecksumVersion(uint32 data_checksum_version)
+SetLocalDataChecksumState(uint32 data_checksum_version)
{
- LocalDataChecksumVersion = data_checksum_version;
+ LocalDataChecksumState = data_checksum_version;
data_checksums = data_checksum_version;
}
@@ -4987,13 +4955,13 @@ SetLocalDataChecksumVersion(uint32 data_checksum_version)
const char *
show_data_checksums(void)
{
- switch (LocalDataChecksumVersion)
+ switch (LocalDataChecksumState)
{
case PG_DATA_CHECKSUM_VERSION:
return "on";
- case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
return "inprogress-on";
- case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
return "inprogress-off";
case PG_DATA_CHECKSUM_OFF:
return "off";
@@ -5298,7 +5266,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
- SetLocalDataChecksumVersion(ControlFile->data_checksum_version);
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5471,7 +5439,7 @@ XLOGShmemInit(void)
/* Use the checksum info from control file */
XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
- SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
@@ -5547,7 +5515,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
- checkPoint.dataChecksumVersion = data_checksum_version;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6635,18 +6603,18 @@ StartupXLOG(void)
* restart here since we cannot launch a dynamic background worker
* directly from here (it has to be from a regular backend).
*/
- if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
{
XLogChecksums(PG_DATA_CHECKSUM_OFF);
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->data_checksum_version = 0;
- SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
ereport(WARNING,
- errmsg("data checksums state has been set to off"),
- errhint("If checksums were being enabled during shutdown then processing must be manually restarted."));
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
}
/*
@@ -6655,13 +6623,13 @@ StartupXLOG(void)
* checksums and we can move to off instead of prompting the user to
* perform any action.
*/
- if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION)
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
{
XLogChecksums(PG_DATA_CHECKSUM_OFF);
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->data_checksum_version = 0;
- SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
}
@@ -7552,7 +7520,7 @@ CreateCheckPoint(int flags)
* Get the current data_checksum_version value from xlogctl, valid at the
* time of the checkpoint.
*/
- checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
if (shutdown)
{
@@ -7674,7 +7642,7 @@ CreateCheckPoint(int flags)
LWLockRelease(OidGenLock);
SpinLockAcquire(&XLogCtl->info_lck);
- checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
SpinLockRelease(&XLogCtl->info_lck);
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
@@ -7827,7 +7795,7 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPointTLI = 0;
/* make sure we start with the checksum version as of the checkpoint */
- ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
@@ -8321,7 +8289,7 @@ CreateRestartPoint(int flags)
}
/* we shall start with the latest checksum version */
- ControlFile->data_checksum_version = lastCheckPoint.dataChecksumVersion;
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
UpdateControlFile();
}
@@ -8770,7 +8738,7 @@ XLogChecksums(uint32 new_type)
xl_checksum_state xlrec;
XLogRecPtr recptr;
- xlrec.new_checksumtype = new_type;
+ xlrec.new_checksum_state = new_type;
XLogBeginInsert();
XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
@@ -8906,8 +8874,8 @@ xlog_redo(XLogReaderState *record)
checkPoint.oldestMultiDB);
SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
- SetLocalDataChecksumVersion(checkPoint.dataChecksumVersion);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
SpinLockRelease(&XLogCtl->info_lck);
/*
@@ -8969,7 +8937,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
- ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -9038,7 +9006,7 @@ xlog_redo(XLogReaderState *record)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
old_state = ControlFile->data_checksum_version;
- ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -9055,21 +9023,21 @@ xlog_redo(XLogReaderState *record)
* refactoring to be pretty (as well as in the _REDO case
*/
SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
- if (checkPoint.dataChecksumVersion != old_state)
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
new_state = true;
SpinLockRelease(&XLogCtl->info_lck);
if (new_state)
{
- switch (checkPoint.dataChecksumVersion)
+ switch (checkPoint.dataChecksumState)
{
- case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
WaitForProcSignalBarrier(barrier);
break;
- case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
WaitForProcSignalBarrier(barrier);
break;
@@ -9263,12 +9231,12 @@ xlog_redo(XLogReaderState *record)
{
switch (redo_rec.data_checksum_version)
{
- case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
WaitForProcSignalBarrier(barrier);
break;
- case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
WaitForProcSignalBarrier(barrier);
break;
@@ -9355,7 +9323,7 @@ xlog2_redo(XLogReaderState *record)
* like that), could that confuse the instance?
*/
SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = state.new_checksumtype;
+ XLogCtl->data_checksum_version = state.new_checksum_state;
SpinLockRelease(&XLogCtl->info_lck);
/*
@@ -9363,14 +9331,14 @@ xlog2_redo(XLogReaderState *record)
* change to checksum status. Once the barrier has been passed we can
* initiate the corresponding processing.
*/
- switch (state.new_checksumtype)
+ switch (state.new_checksum_state)
{
- case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
WaitForProcSignalBarrier(barrier);
break;
- case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
WaitForProcSignalBarrier(barrier);
break;
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 9803ece0792..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -86,7 +86,7 @@ AuxiliaryProcessMainCommon(void)
* NB: Even if the postmaster handled barriers, the value might still be
* stale, as it might have changed after this process forked.
*/
- InitLocalDataChecksumVersion();
+ InitLocalDataChecksumState();
/*
* Auxiliary processes don't run transactions, but they may need a
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 2384be7f657..2b6c412b70a 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -4,7 +4,7 @@
* Background worker for enabling or disabling data checksums online as
* well as functionality for manipulating data checksum state
*
- * When enabling data checksums on a database at initdb time or when shut down
+ * When enabling data checksums on a cluster at initdb time or when shut down
* with pg_checksums, no extra process is required as each page is checksummed,
* and verified, when accessed. When enabling checksums on an already running
* cluster, this worker will ensure that all pages are checksummed before
@@ -21,18 +21,20 @@
* "inprogress-on" which signals that write operations MUST compute and write
* the checksum on the data page, but during reading the checksum SHALL NOT be
* verified. This ensures that all objects created during when checksums are
- * being enabled will have checksums set, but reads wont fail due to missing or
+ * being enabled will have checksums set, but reads won't fail due to missing or
* invalid checksums. Invalid checksums can be present in case the cluster had
* checksums enabled, then disabled them and updated the page while they were
* disabled.
*
- * The DataChecksumsWorker will compile a list of databases which exist at the
- * start of checksumming, and once all are processed will regenerate the list
- * and start over processing any new entries. Once there are no new entries on
- * the list, processing will end. All databases MUST BE successfully processed
- * in order for data checksums to be enabled, the only exception are databases
- * which are dropped before having been processed.
-
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * and once all are processed will regenerate the list and start over
+ * processing any new entries. Once there are no new entries on the list,
+ * processing will end. The regenerated list is required since databases can
+ * be created concurrently with data checksum processing, using a template
+ * database which has yet to be processed. All databases MUST BE successfully
+ * processed in order for data checksums to be enabled, the only exception are
+ * databases which are dropped before having been processed.
+ *
* Any new relation in a processed database, created during processing, will
* see the in-progress state and will automatically be checksummed.
*
@@ -46,13 +48,13 @@
* 2. Disabling checksums
* ----------------------
* When disabling checksums, data_checksums will be set to "inprogress-off"
- * which signals that checksums are written but no longer verified. This ensure
- * that backends which have yet to move from the "on" state will still be able
- * to process data checksum validation.
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
*
* 3. Synchronization and Correctness
* ----------------------------------
- * The processes involved in enabling, or disabling, data checksums in an
+ * The processes involved in enabling or disabling data checksums in an
* online cluster must be properly synchronized with the normal backends
* serving concurrent queries to ensure correctness. Correctness is defined
* as the following:
@@ -74,8 +76,8 @@
* latter with ensuring that any concurrent activity cannot break the data
* checksum contract during processing.
*
- * Synchronizing the state change is done with procsignal barriers, before
- * updating the controlfile with the state all other backends must absorb the
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
* barrier. Barrier absorption will happen during interrupt processing, which
* means that connected backends will change state at different times. If
* waiting for a barrier is done during startup, for example during replay, it
@@ -90,8 +92,9 @@
* failing to validate the data checksum on the page when reading it.
*
* When processing starts all backends belong to one of the below sets, with
- * one set being empty:
+ * one if Bd and Bi being empty:
*
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
* Bd: Backends in "off" state
* Bi: Backends in "inprogress-on" state
*
@@ -173,6 +176,9 @@
* * Restartability (not necessarily with page granularity).
* * Avoid processing databases which were created during inprogress-on.
* Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
*
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -229,8 +235,7 @@
* must not be in.
*
* The reason for this explicit checking is to ensure that processing cannot
- * be started such that it breaks the assumptions of the state machine. See
- * datachecksumsworker.c for a lengthy discussion on these states.
+ * be started such that it breaks the assumptions of the state machine.
*
* MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
* barrier_ne in the below checksum_barriers definition.
@@ -252,10 +257,41 @@ typedef struct ChecksumBarrierCondition
static const ChecksumBarrierCondition checksum_barriers[4] =
{
- {PG_DATA_CHECKSUM_OFF, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION}, 2, {PG_DATA_CHECKSUM_VERSION}, 1},
- {PG_DATA_CHECKSUM_VERSION, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION}, 1, {0}, 0},
- {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, {0}, 0, {PG_DATA_CHECKSUM_VERSION}, 1},
- {PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, {PG_DATA_CHECKSUM_VERSION}, 1, {0}, 0},
+ /*
+ * When disabling checksums, either inprogress state is Ok but checksums
+ * must not be in the enabled state.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ .barrier_eq_sz = 2,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ /* When enabling the current state must be inprogress-on */
+ {
+ .target = PG_DATA_CHECKSUM_VERSION,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+
+ /*
+ * When moving to inprogress-on the current state cannot enabled, but when
+ * moving to inprogress-off the current state must be enabled.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
+ {0}, 0,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
};
/*
@@ -399,13 +435,13 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
switch (barrier)
{
case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
- target_state = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
break;
case PROCSIGNAL_BARRIER_CHECKSUM_ON:
target_state = PG_DATA_CHECKSUM_VERSION;
break;
case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
- target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
break;
case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
target_state = PG_DATA_CHECKSUM_OFF;
@@ -427,7 +463,7 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
*/
if (RecoveryInProgress())
{
- SetLocalDataChecksumVersion(target_state);
+ SetLocalDataChecksumState(target_state);
return true;
}
@@ -480,7 +516,7 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
errmsg("incorrect data checksum state %i for target state %i",
current, target_state));
- SetLocalDataChecksumVersion(target_state);
+ SetLocalDataChecksumState(target_state);
return true;
}
@@ -644,7 +680,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
return false;
/* Report the current relation to pgstat_activity */
- snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %dblocks)",
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
relns, RelationGetRelationName(reln), forkNames[forkNum], numblocks);
pgstat_report_activity(STATE_RUNNING, activity);
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
@@ -658,7 +694,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
{
Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
- /* Need to get an exclusive lock before we can flag as dirty */
+ /* Need to get an exclusive lock to mark the buffer as dirty */
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
/*
@@ -666,7 +702,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
* re-write the page to WAL even if the checksum hasn't changed,
* because if there is a replica it might have a slightly different
* version of the page with an invalid checksum, caused by unlogged
- * changes (e.g. hintbits) on the master happening while checksums
+ * changes (e.g. hintbits) on the primary happening while checksums
* were off. This can happen if there was a valid checksum on the page
* at one point in the past, so only when checksums are first on, then
* off, and then turned on again. TODO: investigate if this could be
@@ -682,8 +718,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
/*
* This is the only place where we check if we are asked to abort, the
- * abortion will bubble up from here. It's safe to check this without
- * a lock, because if we miss it being set, we will try again soon.
+ * abortion will bubble up from here.
*/
Assert(operation == ENABLE_DATACHECKSUMS);
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
@@ -827,10 +862,9 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
/*
* If the postmaster crashed we cannot end up with a processed database so
* we have no alternative other than exiting. When enabling checksums we
- * won't at this time have changed the pg_control version to enabled so
- * when the cluster comes back up processing will have to be restarted.
- * When disabling, the pg_control version will be set to off before this
- * so when the cluster comes up checksums will be off as expected.
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
*/
if (status == BGWH_POSTMASTER_DIED)
ereport(FATAL,
@@ -879,6 +913,8 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
static void
launcher_exit(int code, Datum arg)
{
+ abort_requested = false;
+
if (launcher_running)
{
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
@@ -1325,7 +1361,7 @@ ProcessAllDatabases(void)
* Get a fresh list of databases to detect the second case where the
* database was dropped before we had started processing it. If a database
* still exists, but enabling checksums failed then we fail the entire
- * checksumming process and exit with an error.
+ * checksum enablement process and exit with an error.
*/
DatabaseList = BuildDatabaseList();
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index c898c53b623..04a3ba04332 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2999,7 +2999,7 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
- /* also add checksumming processes */
+ /* also add data checksums processes */
remainMask = btmask_add(remainMask,
B_DATACHECKSUMSWORKER_LAUNCHER,
B_DATACHECKSUMSWORKER_WORKER);
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 8da8f8fecf1..4941d8e224f 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,6 +107,13 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
HOLD_INTERRUPTS();
if (DataChecksumsNeedVerify())
{
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index edd429e83cb..18f77bfb5b4 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -774,7 +774,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
* NB: Even if the postmaster handled barriers, the value might still be
* stale, as it might have changed after this process forked.
*/
- InitLocalDataChecksumVersion();
+ InitLocalDataChecksumState();
/*
* Also set up timeout handlers needed for backend operation. We need
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 661b8757979..5e58ca1345f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -504,8 +504,8 @@ static const struct config_enum_entry file_extend_method_options[] = {
static const struct config_enum_entry data_checksums_options[] = {
{"on", PG_DATA_CHECKSUM_VERSION, true},
{"off", PG_DATA_CHECKSUM_OFF, true},
- {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, true},
- {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
{NULL, 0, false}
};
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index 47afa82d9c5..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -288,7 +288,7 @@ main(int argc, char *argv[])
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
printf(_("Latest checkpoint's data_checksum_version:%u\n"),
- ControlFile->checkPointCopy.dataChecksumVersion);
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 7cec6301769..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -26,7 +26,6 @@
/* symbol name, textual name, redo, desc, identify, startup, cleanup, mask, decode */
PG_RMGR(RM_XLOG_ID, "XLOG", xlog_redo, xlog_desc, xlog_identify, NULL, NULL, NULL, xlog_decode)
-PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
PG_RMGR(RM_XACT_ID, "Transaction", xact_redo, xact_desc, xact_identify, NULL, NULL, NULL, xact_decode)
PG_RMGR(RM_SMGR_ID, "Storage", smgr_redo, smgr_desc, smgr_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_CLOG_ID, "CLOG", clog_redo, clog_desc, clog_identify, NULL, NULL, NULL, NULL)
@@ -48,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 256f3499481..684c1a9b20a 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -249,14 +249,12 @@ extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
extern bool DataChecksumsNeedWrite(void);
extern bool DataChecksumsNeedVerify(void);
-extern bool DataChecksumsOnInProgress(void);
-extern bool DataChecksumsOffInProgress(void);
extern void SetDataChecksumsOnInProgress(void);
extern void SetDataChecksumsOn(void);
extern void SetDataChecksumsOff(void);
extern const char *show_data_checksums(void);
-extern void InitLocalDataChecksumVersion(void);
-extern void SetLocalDataChecksumVersion(uint32 data_checksum_version);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 8fb5b978f51..dfa0251d55e 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -291,7 +291,7 @@ typedef struct xl_restore_point
/* Information logged when data checksum level is changed */
typedef struct xl_checksum_state
{
- ChecksumType new_checksumtype;
+ ChecksumStateType new_checksum_state;
} xl_checksum_state;
/* Overwrite of prior contrecord */
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 5ac74d7ef1c..146c06cc8ef 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -65,7 +65,7 @@ typedef struct CheckPoint
TransactionId oldestActiveXid;
/* data checksums state at the time of the checkpoint */
- uint32 dataChecksumVersion;
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -229,7 +229,7 @@ typedef struct ControlFileData
bool float8ByVal; /* float8, int8, etc pass-by-value? */
/* Are data pages protected by checksums? Zero if no checksum version */
- uint32 data_checksum_version; /* persistent */
+ uint32 data_checksum_version;
/*
* True if the default signedness of char is "signed" on a platform where
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index b6bbba82949..fe5d30b4349 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -16,18 +16,18 @@
#include "storage/block.h"
/*
- * Checksum version 0 is used for when data checksums are disabled (OFF).
- * PG_DATA_CHECKSUM_VERSION defines that data checksums are enabled in the
- * cluster and PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF}_VERSION defines that data
- * checksums are either currently being enabled or disabled.
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled.
*/
-typedef enum ChecksumType
+typedef enum ChecksumStateType
{
PG_DATA_CHECKSUM_OFF = 0,
PG_DATA_CHECKSUM_VERSION,
- PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION,
- PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION
-} ChecksumType;
+ PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ PG_DATA_CHECKSUM_INPROGRESS_ON,
+} ChecksumStateType;
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
index a4d35e9f95c..2b36e5ee59a 100644
--- a/src/test/modules/test_checksums/t/002_restarts.pl
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -36,7 +36,7 @@ SKIP:
if (!$ENV{PG_TEST_EXTRA}
|| $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
- # Create a barrier for checksumming to block on, in this case a pre-
+ # Create a barrier for checksum enablement to block on, in this case a pre-
# existing temporary table which is kept open while processing is started.
# We can accomplish this by setting up an interactive psql process which
# keeps the temporary table created as we enable checksums in another psql
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
index f33e278de28..afe6538faab 100644
--- a/src/test/modules/test_checksums/t/004_offline.pl
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -48,9 +48,9 @@ $node->start;
# Ensure that checksums are disabled
test_checksum_state($node, 'off');
-# Create a barrier for checksumming to block on, in this case a pre-existing
-# temporary table which is kept open while processing is started. We can
-# accomplish this by setting up an interactive psql process which keeps the
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
# temporary table created as we enable checksums in another psql process.
my $bsession = $node->background_psql('postgres');
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c2f9a2a9b9a..15d32a7768b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -432,7 +432,7 @@ CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
ChecksumBarrierCondition
-ChecksumType
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-19 13:49 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-03-19 13:49 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 18 Mar 2026, at 09:06, Heikki Linnakangas <[email protected]> wrote:
> On 18/03/2026 00:01, Daniel Gustafsson wrote:
>>> What if a database is dropped and another one is created with the same database OID?
>> Good point. We have the name of the database so we could compare Oid and name,
>> and require both to match. That would still be foiled by a new database using
>> the same Oid and name though. Any ideas on what could be done?
> Well, if CREATE DATABASE computed the checksums, that would fix this too. Actually, at least in the default wal_log=true mode, doesn't it already do that? It goes through the buffer cache as usual, I presume the checksums will be computed too.
That's a very good point, if the CREATE DATABASE is issued with wal_log and not
file_copy it will mark the buffers dirty during copying which will calculate
and set the checkpoint. If file_copy isn't allowed during inprogress-on then
the logic around rescanning databases for new entries could be quite
simplified.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-19 23:34 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-03-19 23:34 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 19 Mar 2026, at 14:49, Daniel Gustafsson <[email protected]> wrote:
>> On 18 Mar 2026, at 09:06, Heikki Linnakangas <[email protected]> wrote:
>> Well, if CREATE DATABASE computed the checksums, that would fix this too. Actually, at least in the default wal_log=true mode, doesn't it already do that? It goes through the buffer cache as usual, I presume the checksums will be computed too.
>
> That's a very good point, if the CREATE DATABASE is issued with wal_log and not
> file_copy it will mark the buffers dirty during copying which will calculate
> and set the checkpoint. If file_copy isn't allowed during inprogress-on then
> the logic around rescanning databases for new entries could be quite
> simplified.
I tried to implement this on top of a fresh rebase, the attached 0003 contains
a very lightly tested version of this simplified logic (there is a removal of a
useless debug logging as well which while unrelated happened to sneak into this
work).
It now works off a single databaselist and relies on wal_copy to handle any
created database regardless of which template is used (file_copy is not allowed
while inprogress-on). This gives a chance to do better erroring out in case of
failed databases, the code is still vulnerable to a the DROP/CREATE with the
same Oid, but that seems doable to solve.
It needs more testing, and likely has bugs, but I wanted to share it as soon as
possible to see if was along what you were thinking. Personally I think the
code a lot easier to follow with this.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260319-0001-Add-proper-WAL-record-for-XLOG_CHECKPOINT_.patch (3.9K, ../../[email protected]/2-v20260319-0001-Add-proper-WAL-record-for-XLOG_CHECKPOINT_.patch)
download | inline diff:
From b71a70e0e7f40b3dc63885b6c2ef7e833fa18ffb Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 12 Mar 2026 18:33:02 +0100
Subject: [PATCH v20260319 1/3] Add proper WAL record for XLOG_CHECKPOINT_REDO
XLOG_CHECKPOINT_REDO only contains the wal_level copied straight in
without an encapsulating record structure. While it works, it makes
future uses of XLOG_CHECKPOINT_REDO hard as there is nowhere to put
new data items. This fix this was inspired by the online checksums
patch which adds data to this record, but this change has value on
its own.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/..
---
src/backend/access/rmgrdesc/xlogdesc.c | 6 +++---
src/backend/access/transam/xlog.c | 8 +++++++-
src/backend/postmaster/walsummarizer.c | 5 ++++-
src/include/access/xlog_internal.h | 6 ++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 9044b952117..44194d3ea17 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -163,10 +163,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- int wal_level;
+ xl_checkpoint_redo xlrec;
- memcpy(&wal_level, rec, sizeof(int));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(wal_level));
+ memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
+ appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f5c9a34374d..2c1c6f88b74 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7179,9 +7179,15 @@ CreateCheckPoint(int flags)
*/
if (!shutdown)
{
+ xl_checkpoint_redo redo_rec;
+
+ WALInsertLockAcquire();
+ redo_rec.wal_level = wal_level;
+ WALInsertLockRelease();
+
/* Include WAL level in record for WAL summarizer's benefit. */
XLogBeginInsert();
- XLogRegisterData(&wal_level, sizeof(wal_level));
+ XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
/*
diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c
index e1aa102f41d..bdc2236a8c1 100644
--- a/src/backend/postmaster/walsummarizer.c
+++ b/src/backend/postmaster/walsummarizer.c
@@ -1428,8 +1428,11 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward)
if (info == XLOG_CHECKPOINT_REDO)
{
+ xl_checkpoint_redo xlrec;
+
/* Payload is wal_level at the time record was written. */
- memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int));
+ memcpy(&xlrec, XLogRecGetData(xlogreader), sizeof(xl_checkpoint_redo));
+ record_wal_level = xlrec.wal_level;
}
else if (info == XLOG_CHECKPOINT_SHUTDOWN)
{
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 629ac3a7d3e..faabba73ef2 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -303,6 +303,12 @@ typedef struct xl_end_of_recovery
int wal_level;
} xl_end_of_recovery;
+/* checkpoint redo */
+typedef struct xl_checkpoint_redo
+{
+ int wal_level;
+} xl_checkpoint_redo;
+
/*
* The functions in xloginsert.c construct a chain of XLogRecData structs
* to represent the final WAL record.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a4a2ed07816..236a75fad6c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4385,6 +4385,7 @@ xl_btree_split
xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
+xl_checkpoint_redo
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
[application/octet-stream] v20260319-0002-Online-enabling-and-disabling-of-data-chec.patch (231.4K, ../../[email protected]/3-v20260319-0002-Online-enabling-and-disabling-of-data-chec.patch)
download | inline diff:
From f1d371ae97325039f620aca5dd9d0229b1f1a8ec Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 15 Aug 2025 14:48:02 +0200
Subject: [PATCH v20260319 2/3] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calcuated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests for online processing are gated begind the
PG_TEST_EXTRA flag to some degree due to being very time consuming
to run.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 77 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 12 +
doc/src/sgml/wal.sgml | 127 +-
src/backend/access/heap/heapam.c | 3 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 588 +++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 9 +
src/backend/postmaster/datachecksum_state.c | 1736 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 20 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 4 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 1 +
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 15 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/pg_control.h | 6 +
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/proc.h | 2 +-
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 81 +
.../test_checksums/t/006_pgbench_single.pl | 263 +++
.../test_checksums/t/007_pgbench_standby.pl | 385 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 172 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 28 +
.../modules/test_checksums/test_checksums.c | 222 +++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 8 +
78 files changed, 5324 insertions(+), 57 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 8cdd826fbd3..4c083335d89 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..46635d27e94 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,81 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums mode can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums mode to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all data checksums
+ have been calculated, and written, for all pages the cluster will
+ automatically set data checksums mode to <literal>on</literal>.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum mode to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum mode will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index e2db5bcc78c..d9378af0606 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index fd55b9ad23f..e805487344d 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 462019a972c..df7e8d2b2ea 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3781,9 +3781,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3792,8 +3797,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7524,6 +7529,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index e9e393495df..e764b8be04d 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..4a006d057c9 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject delays
+ and re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..df41fb54cb6 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases or tables.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,124 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will put the cluster checksum mode in
+ <literal>inprogress-on</literal> mode. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum mode will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in the application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> mode,
+ for any reason, then the checksum enable process must be restarted manually.
+ To do this, re-execute the function <function>pg_enable_data_checksums()</function>
+ once the cluster has been restarted. The process will start over, there is
+ no support for resuming work from where it was interrupted. If the cluster
+ is stopped while in <literal>inprogress-off</literal>, then the checksum
+ state will be set to <literal>off</literal> when the cluster is
+ restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum mode to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the mode will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing. There is no way to restart from where
+ processing was interrupted.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ Rewriting all pages is not needed when disabling data checksums, but
+ checkpoints are still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index e5bd062de77..317dab3a3d2 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -6031,8 +6031,7 @@ l4:
&new_xmax, &new_infomask, &new_infomask2);
if (PageIsAllVisible(BufferGetPage(buf)) &&
- visibilitymap_clear(rel, block, vmbuffer,
- VISIBILITYMAP_ALL_FROZEN))
+ visibilitymap_clear(rel, block, vmbuffer, VISIBILITYMAP_ALL_FROZEN))
cleared_all_frozen = true;
START_CRIT_SECTION();
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..47c3c8363ed 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+static const char *
+get_checksum_version_string(ChecksumStateType checksum)
+{
+ switch (checksum)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_version_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_version_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_version_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..883904d390f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -92,6 +92,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +554,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +654,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +736,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -831,7 +852,7 @@ XLogInsertRecord(XLogRecData *rdata,
* only happen just after a checkpoint, so it's better to be slow in
* this case and fast otherwise.
*
- * Also check to see if fullPageWrites was just turned on or there's a
+ * Also check to see if fullPageWrites was just turned on, there's a
* running backup (which forces full-page writes); if we weren't
* already doing full-page writes then go back and recompute.
*
@@ -4254,6 +4275,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4615,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4648,331 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true iff data checksums are enabled or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Enables data checksums cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified. See datachecksum_state.c for a longer discussion on
+ * how data checksums can be enabled in a running cluster.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition of "on" in all backends. When done we know that
+ * data checksums are enabled in all backends and data checksums are both
+ * written and verified.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ switch (LocalDataChecksumState)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ /*
+ * Since all states are covered in the switch statement, this cannot
+ * happen.
+ */
+ Assert(false);
+ return "?";
}
/*
@@ -4925,6 +5266,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5436,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5515,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6592,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6938,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7516,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7578,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7641,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7794,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7940,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8287,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8729,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8873,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8937,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8964,9 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
+ uint64 barrier;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +9005,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9018,42 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * XXX: I'm not sure this is right, and if it is this needs
+ * refactoring to be pretty (as well as in the _REDO case
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ {
+ switch (checkPoint.dataChecksumState)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9215,43 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+ uint64 barrier;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ {
+ switch (redo_rec.data_checksum_version)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9304,58 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+ uint64 barrier;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ /*
+ * XXX Could this end up written to the control file prematurely? IIRC
+ * that happens during checkpoint, so what if that gets triggered e.g.
+ * because someone runs CHECKPOINT? If we then crash (or something
+ * like that), could that confuse the instance?
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ switch (state.new_checksum_state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+ }
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..cdbaa0c0337 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point were we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f1ed7b58f13..c500e41e2c5 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1442,6 +1442,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index d1fe3cc71ce..33a70124489 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -143,6 +144,14 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
}
};
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..917cbb7f354
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1736 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * and once all are processed will regenerate the list and start over
+ * processing any new entries. Once there are no new entries on the list,
+ * processing will end. The regenerated list is required since databases can
+ * be created concurrently with data checksum processing, using a template
+ * database which has yet to be processed. All databases MUST BE successfully
+ * processed in order for data checksums to be enabled, the only exception are
+ * databases which are dropped before having been processed.
+ *
+ * Any new relation in a processed database, created during processing, will
+ * see the in-progress state and will automatically be checksummed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set of "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the set of conditions to use is
+ * looked up in the checksum_barriers struct. The struct member for the target
+ * state defines which state the backend must currently be in, and which it
+ * must not be in.
+ *
+ * The reason for this explicit checking is to ensure that processing cannot
+ * be started such that it breaks the assumptions of the state machine.
+ *
+ * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
+ * barrier_ne in the below checksum_barriers definition.
+ */
+#define MAX_BARRIER_CONDITIONS 2
+typedef struct ChecksumBarrierCondition
+{
+ /* The target state of the barrier */
+ int target;
+ /* A set of states in which at least one MUST match the current state */
+ int barrier_eq[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_eq set */
+ int barrier_eq_sz;
+ /* A set of states which all MUST NOT match the current state */
+ int barrier_ne[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_ne set */
+ int barrier_ne_sz;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[4] =
+{
+ /*
+ * When disabling checksums, either inprogress state is Ok but checksums
+ * must not be in the enabled state.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ .barrier_eq_sz = 2,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ /* When enabling the current state must be inprogress-on */
+ {
+ .target = PG_DATA_CHECKSUM_VERSION,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+
+ /*
+ * When moving to inprogress-on the current state cannot enabled, but when
+ * moving to inprogress-off the current state must be enabled.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
+ {0}, 0,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+};
+
+/*
+ * Number of times we retry to open a database before giving up and consider
+ * it to have failed processing.
+ */
+#define DATACHECKSUMSWORKER_MAX_DB_RETRIES 5
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsWorkerShmemStruct
+{
+ /*
+ * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * If a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ bool worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsWorkerShmemStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+typedef struct DataChecksumsWorkerResultEntry
+{
+ Oid dboid;
+ DataChecksumsWorkerResult result;
+ int retries;
+} DataChecksumsWorkerResultEntry;
+
+
+/*
+ * Flag set by the interrupt handler
+ */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/*
+ * Are we enabling data checksums, or disabling them?
+ */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ *
+ */
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ const ChecksumBarrierCondition *condition = NULL;
+ int target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers); i++)
+ {
+ if (checksum_barriers[i].target == target_state)
+ condition = &checksum_barriers[i];
+ }
+ Assert(condition);
+
+ /*
+ * The current state MUST be equal to one of the EQ states defined in this
+ * barrier condition. If the EQ states array is zero then that implies
+ * that the current state can match any state, so fastpath check for that
+ * first.
+ */
+ if (condition->barrier_eq_sz == 0)
+ found = true;
+ else
+ {
+ for (int i = 0; i < condition->barrier_eq_sz; i++)
+ {
+ if (current == condition->barrier_eq[i])
+ found = true;
+ }
+ }
+
+ /*
+ * The current state MUST NOT be equal to any of the NE states defined in
+ * this barrier condition.
+ */
+ for (int i = 0; i < condition->barrier_ne_sz; i++)
+ {
+ if (current == condition->barrier_ne[i])
+ found = false;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ *
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumsWorkerShmem->launch_operation = op;
+ DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
+ DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumsWorkerShmem->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ if (!relns)
+ return false;
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ relns, RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /*
+ * As of now we only update the block counter for main forks in order
+ * to not cause too frequent calls. TODO: investigate whether we
+ * should do it more frequent?
+ */
+ if (forkNum == MAIN_FORKNUM)
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ pfree(relns);
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, make sure we retry processing
+ * this database. This will make the datachecksumsworker move on to the
+ * next database and quite likely fail with the same problem. TODO: Maybe
+ * we need a backoff to avoid running through all the databases here in
+ * short order.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("failed to start worker for enabling data checksums in database \"%s\", retrying",
+ db->dbname),
+ errhint("The max_worker_processes setting might be too low."));
+ return DATACHECKSUMSWORKER_RETRYDB;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ DataChecksumsWorkerShmem->worker_running = true;
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ DataChecksumsWorkerShmem->worker_running = false;
+
+ return DataChecksumsWorkerShmem->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+
+ /*
+ * TODO: how to really handle the worker still running when the
+ * launcher exits?
+ */
+ if (DataChecksumsWorkerShmem->worker_running)
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running"));
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumsWorkerShmem->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->launcher_running = true;
+ DataChecksumsWorkerShmem->operation = operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will repeatedly generate a list of databases to process for enabling
+ * checksums. Until no new databases are found, this will loop around computing
+ * a new list and comparing it to the already seen ones.
+ *
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ HTAB *ProcessedDatabases = NULL;
+ HASHCTL hash_ctl;
+ bool found_failed = false;
+ int cumulative_total = 0;
+
+ /* Initialize a hash tracking all processed databases */
+ memset(&hash_ctl, 0, sizeof(hash_ctl));
+ hash_ctl.keysize = sizeof(Oid);
+ hash_ctl.entrysize = sizeof(DataChecksumsWorkerResultEntry);
+ ProcessedDatabases = hash_create("Processed databases",
+ 64,
+ &hash_ctl,
+ HASH_ELEM | HASH_BLOBS);
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumsWorkerShmem->process_shared_catalogs = true;
+
+ /*
+ * Get a list of all databases to process. This may include databases that
+ * were created during our runtime. Since a database can be created as a
+ * copy of any other database (which may not have existed in our last
+ * run), we have to repeat this loop until no new databases show up in the
+ * list. Here the initial list for the loop processing is generated after
+ * waiting for all existing transactions to finish to ensure that we can
+ * see any database which was created even if the transaction in which it
+ * was created started before checksums were being processed.
+ */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /* Allow a test case to modify the initial list of databases */
+ INJECTION_POINT("datachecksumsworker-initial-dblist", &DatabaseList);
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ while (true)
+ {
+ int processed_databases = 0;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+ DataChecksumsWorkerResultEntry *entry;
+ bool found;
+
+ /*
+ * Check if this database has been processed already, and if so
+ * whether it should be retried or skipped.
+ */
+ entry = (DataChecksumsWorkerResultEntry *) hash_search(ProcessedDatabases, &db->dboid,
+ HASH_FIND, NULL);
+
+ if (entry)
+ {
+ if (entry->result == DATACHECKSUMSWORKER_RETRYDB)
+ {
+ /*
+ * Limit the number of retries to avoid infinite looping
+ * in case there simply won't be enough workers in the
+ * cluster to finish this operation.
+ */
+ if (entry->retries > DATACHECKSUMSWORKER_MAX_DB_RETRIES)
+ entry->result = DATACHECKSUMSWORKER_FAILED;
+ }
+
+ /* Skip if this database has been processed already */
+ if (entry->result != DATACHECKSUMSWORKER_RETRYDB)
+ continue;
+ }
+
+ result = ProcessDatabase(db);
+ processed_databases++;
+ cumulative_total++;
+
+ /*
+ * Update the number of processed databases in the progress
+ * report.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ cumulative_total);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-fail-db", &result);
+
+ if (result == DATACHECKSUMSWORKER_SUCCESSFUL)
+ {
+ /*
+ * If one database has completed shared catalogs, we don't
+ * have to process them again.
+ */
+ if (DataChecksumsWorkerShmem->process_shared_catalogs)
+ DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ entry = hash_search(ProcessedDatabases, &db->dboid, HASH_ENTER, &found);
+ entry->dboid = db->dboid;
+ entry->result = result;
+ if (!found)
+ entry->retries = 0;
+ else
+ entry->retries++;
+ }
+
+ elog(DEBUG1,
+ "%i databases processed for data checksum enabling (%i this iteration), %s",
+ cumulative_total, processed_databases,
+ (processed_databases ? "process with restart" : "process completed"));
+
+ FreeDatabaseList(DatabaseList);
+
+ /*
+ * If no databases were processed in this run of the loop, we have now
+ * finished all databases and no concurrently created ones can exist.
+ */
+ if (processed_databases == 0)
+ break;
+
+ /*
+ * Re-generate the list of databases for another pass in case any new
+ * databases were created while we were running. Since the initial
+ * list was generated after waiting for all transaction to finish we
+ * know that all new databases found here must have been created while
+ * seeing the new checksum state. By waiting for all transactions
+ * here as well we know that any database created using an existing db
+ * as a template (which may have been used before it had checksums
+ * enabled) will be committed.
+ */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+ }
+
+ /*
+ * ProcessedDatabases now has all databases and the results of their
+ * processing. Failure to enable checksums for a database can be because
+ * they actually failed for some reason, or because the database was
+ * dropped between us getting the database list and trying to process it.
+ * Get a fresh list of databases to detect the second case where the
+ * database was dropped before we had started processing it. If a database
+ * still exists, but enabling checksums failed then we fail the entire
+ * checksum enablement process and exit with an error.
+ */
+ DatabaseList = BuildDatabaseList();
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResultEntry *entry;
+ bool found;
+
+ entry = hash_search(ProcessedDatabases, (void *) &db->dboid,
+ HASH_FIND, &found);
+
+ /*
+ * We are only interested in the processed databases which failed, and
+ * where the failed database still exists. This indicates that
+ * enabling checksums actually failed, and not that the failure was
+ * due to the db being concurrently dropped.
+ */
+ if (found && entry->result == DATACHECKSUMSWORKER_FAILED)
+ {
+ ereport(WARNING,
+ errmsg("failed to enable data checksums in \"%s\"", db->dbname));
+ found_failed = found;
+ continue;
+ }
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ if (found_failed)
+ {
+ /* Disable checksums on cluster, because we failed */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumsWorkerShmemSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsWorkerShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumsWorkerShmemInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsWorkerShmemInit(void)
+{
+ bool found;
+
+ DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsWorkerShmemSize(),
+ &found);
+
+ if (!found)
+ MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
+ VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumsWorkerShmem->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3fac46c402b..04a3ba04332 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2999,6 +2999,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 21f03864a66..4674c0758c1 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index a4785daf1e5..bbd59d8af44 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsWorkerShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -316,6 +318,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsWorkerShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index de85911e3ac..4941d8e224f 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -1511,7 +1520,7 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
static char *pageCopy = NULL;
/* If we don't need a checksum, just return the passed-in data */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
return page;
/*
@@ -1540,9 +1549,14 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
void
PageSetChecksumInplace(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index f2f8d3ff75f..07fb6bacb40 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..8105a0af79b 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,9 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_FINISHCONDITION "Waiting for data checksums to be enabled."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -366,6 +369,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f907335990..e20b02c03da 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 26118661f07..18f77bfb5b4 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 0c9854ad8fc..49332642cae 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -531,11 +531,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e4abe6c0077..aaa277687dd 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 5db5d20136f..33ea514a52d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -53,6 +53,7 @@ command_fails_like(
# case exercising the new rmgr below.
command_like(
[ 'pg_waldump', '--rmgr=list' ], qr/^XLOG
+XLOG2
Transaction
Storage
CLOG
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..684c1a9b20a 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,14 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index faabba73ef2..dfa0251d55e 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..146c06cc8ef 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 84e7adde0e5..5ec23fb2968 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12541,6 +12541,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 's', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..fb4fca24991 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..5601241a803
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsWorkerShmemSize(void);
+extern void DataChecksumsWorkerShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+ /* TODO: VERIFY_DATACHECKSUMS, */
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_RETRYDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index 3de58ba4312..ed7e0abcbfa 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..fe5d30b4349 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ PG_DATA_CHECKSUM_INPROGRESS_ON,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index e94ebce95b9..0c02f2ae38f 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index bf3094f0f7d..e1f8320cdf3 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -524,7 +524,7 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs;
* Background writer, checkpointer, WAL writer, WAL summarizer, and archiver
* run during normal operation. Startup process and WAL receiver also consume
* 2 slots, but WAL writer is launched only after startup has exited, so we
- * only need 6 slots.
+ * only need 6 slots to cover these.
*/
#define MAX_IO_WORKERS 32
#define NUM_AUXILIARY_PROCS (6 + MAX_IO_WORKERS)
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..397f6411cb5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 'on');
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..2b36e5ee59a
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..c5732a2c7da
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby_1->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby_1->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby_1, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby_1, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby_1->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby_1->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby_1, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby_1, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby_1->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..afe6538faab
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..5f7f4102b9e
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+# Force the enable checksums processing to make multiple passes by removing
+# one database from the list in the first pass. This will simulate a CREATE
+# DATABASE during processing. Doing this via fault injection makes the test
+# not be subject to exact timing.
+$node->safe_psql('postgres', 'SELECT dcw_prune_dblist(true);');
+enable_data_checksums($node, wait => 'on');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..727c804bbd5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,263 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test is very expensive to run, so by default it runs a pared-down test
+# suite. Setting $extended to 1 will run the full suite and can be done by
+# setting PG_TEST_EXTRA to "checksum_extended".
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+$node->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..8e099fdbe65
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,385 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby_1;
+my $node_standby_1_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+
+ # Terminate any currently running pgbench process before continuing
+ $pgbench_primary->finish if $pgbench_primary;
+
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ my $clients = 1;
+ my $runtime = 10;
+
+ if ($extended)
+ {
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench_primary = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby_1, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby_1->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby_1->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby_1, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby_1, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby_1, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby_1->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby_1->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby_1->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby_1->is_alive)
+ {
+ $node_standby_1->start;
+ $node_standby_1->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby_1->append_conf('postgresql.conf',
+ qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby_1->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby_1->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby_1, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently)
+ # XXX should maybe try stopping nodes in the opposite order too?
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby_1->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby_1->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_1_loglocation = -s $node_standby_1->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby_1->is_alive)
+{
+ $node_standby_1->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby_1, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+ $node_standby_1_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+$node_standby_1->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..6c98e0ab23d
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,172 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/)
+{
+ plan skip_all => 'Extended checksum tests not enabled';
+}
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ my $clients = 1 + int(rand(15));
+
+ my @cmd = ('pgbench', '-p', $port, '-T', '600', '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if (cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('node_pitr');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..778c696abc6
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 'off');
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..fb1c802461b
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,28 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_prune_dblist(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..03752adff65
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,222 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_dblist(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+dc_fail_database(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+
+ if (first_pass)
+ *res = DATACHECKSUMSWORKER_FAILED;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fail-db",
+ "test_checksums",
+ "dc_fail_database",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fail-db");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to remove an entry from the Databaselist to force re-processing since
+ * not all databases could be processed in the first iteration of the loop.
+ */
+void
+dc_dblist(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ List **DatabaseList = (List **) arg;
+
+ if (first_pass)
+ *DatabaseList = list_delete_last(*DatabaseList);
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_prune_dblist);
+Datum
+dcw_prune_dblist(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-initial-dblist",
+ "test_checksums",
+ "dc_dblist",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-initial-dblist");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index b44aefb545a..1becdaa2e38 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 32bea58db2c..24e68b76f3e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2079,6 +2079,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index b99462bf946..f25394545df 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 236a75fad6c..75fea8416fc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -602,6 +604,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -620,6 +623,10 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsWorkerResultEntry
+DataChecksumsWorkerShmemStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4386,6 +4393,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
[application/octet-stream] v20260319-0003-A-stab-at-simplified-database-processing-l.patch (10.4K, ../../[email protected]/4-v20260319-0003-A-stab-at-simplified-database-processing-l.patch)
download | inline diff:
From 952c3de9dd0f15d8d6a4371568015d539521c58a Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 20 Mar 2026 00:08:29 +0100
Subject: [PATCH v20260319 3/3] A stab at simplified database processing logic
The previous coding extracted a list of databases, and proceeded to
process that list. Once done, it would extract a new list and redo
it all for any new database created concurrently. This was done to
ensure that a database created from a template which hadnt yet been
processed would be covered. Since the wal_log strategy for create
database dirties all buffers during thec copy process, this was all
doing double work. Simplify the logic by only processing a single
list and prohibit the file_copy strategy.
---
src/backend/access/transam/xlog.c | 6 +
src/backend/commands/dbcommands.c | 6 +
src/backend/postmaster/datachecksum_state.c | 132 +++++++++-----------
src/include/access/xlog.h | 1 +
src/include/postmaster/datachecksum_state.h | 1 +
5 files changed, 73 insertions(+), 73 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 883904d390f..5f486200ea3 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4669,6 +4669,12 @@ DataChecksumsNeedWrite(void)
LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
}
+bool
+DataChecksumsInProgress(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
/*
* DataChecksumsNeedVerify
* Returns whether data checksums must be verified or not
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..c8706e16070 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,13 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgress())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are bing enabled", strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 917cbb7f354..477151091a1 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -396,6 +396,7 @@ static volatile sig_atomic_t launcher_running = false;
static DataChecksumsWorkerOperation operation;
/* Prototypes */
+static bool DatabaseExists(Oid dboid);
static List *BuildDatabaseList(void);
static List *BuildRelationList(bool temp_relations, bool include_shared);
static void FreeDatabaseList(List *dblist);
@@ -852,7 +853,15 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
errmsg("could not start background worker for enabling data checksums in database \"%s\"",
db->dbname),
errhint("More details on the error might be found in the server log."));
- return DATACHECKSUMSWORKER_FAILED;
+ /*
+ * XXX: Heuristic to see if the database was dropped, and if so we can
+ * ignore the error, else treat as fatal and error out. This needs a
+ * tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
}
/*
@@ -1170,9 +1179,8 @@ done:
* Compute the list of all databases and process checksums in each
*
* This will repeatedly generate a list of databases to process for enabling
- * checksums. Until no new databases are found, this will loop around computing
- * a new list and comparing it to the already seen ones.
- *
+ * checksums. If a database encounters a failure then processing will end
+ * immediately and return an error.
*/
static bool
ProcessAllDatabases(void)
@@ -1180,7 +1188,6 @@ ProcessAllDatabases(void)
List *DatabaseList;
HTAB *ProcessedDatabases = NULL;
HASHCTL hash_ctl;
- bool found_failed = false;
int cumulative_total = 0;
/* Initialize a hash tracking all processed databases */
@@ -1195,16 +1202,7 @@ ProcessAllDatabases(void)
/* Set up so first run processes shared catalogs, not once in every db */
DataChecksumsWorkerShmem->process_shared_catalogs = true;
- /*
- * Get a list of all databases to process. This may include databases that
- * were created during our runtime. Since a database can be created as a
- * copy of any other database (which may not have existed in our last
- * run), we have to repeat this loop until no new databases show up in the
- * list. Here the initial list for the loop processing is generated after
- * waiting for all existing transactions to finish to ensure that we can
- * see any database which was created even if the transaction in which it
- * was created started before checksums were being processed.
- */
+ /* Get a list of all databases to process */
WaitForAllTransactionsToFinish();
DatabaseList = BuildDatabaseList();
@@ -1303,6 +1301,18 @@ ProcessAllDatabases(void)
/* Abort flag set, so exit the whole process */
return false;
}
+ else if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
entry = hash_search(ProcessedDatabases, &db->dboid, HASH_ENTER, &found);
entry->dboid = db->dboid;
@@ -1313,17 +1323,7 @@ ProcessAllDatabases(void)
entry->retries++;
}
- elog(DEBUG1,
- "%i databases processed for data checksum enabling (%i this iteration), %s",
- cumulative_total, processed_databases,
- (processed_databases ? "process with restart" : "process completed"));
-
- FreeDatabaseList(DatabaseList);
-
- /*
- * If no databases were processed in this run of the loop, we have now
- * finished all databases and no concurrently created ones can exist.
- */
+ /* If no databases were processed in this iteration, exit the loop */
if (processed_databases == 0)
break;
@@ -1338,56 +1338,10 @@ ProcessAllDatabases(void)
* enabled) will be committed.
*/
WaitForAllTransactionsToFinish();
- DatabaseList = BuildDatabaseList();
- }
-
- /*
- * ProcessedDatabases now has all databases and the results of their
- * processing. Failure to enable checksums for a database can be because
- * they actually failed for some reason, or because the database was
- * dropped between us getting the database list and trying to process it.
- * Get a fresh list of databases to detect the second case where the
- * database was dropped before we had started processing it. If a database
- * still exists, but enabling checksums failed then we fail the entire
- * checksum enablement process and exit with an error.
- */
- DatabaseList = BuildDatabaseList();
-
- foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
- {
- DataChecksumsWorkerResultEntry *entry;
- bool found;
-
- entry = hash_search(ProcessedDatabases, (void *) &db->dboid,
- HASH_FIND, &found);
-
- /*
- * We are only interested in the processed databases which failed, and
- * where the failed database still exists. This indicates that
- * enabling checksums actually failed, and not that the failure was
- * due to the db being concurrently dropped.
- */
- if (found && entry->result == DATACHECKSUMSWORKER_FAILED)
- {
- ereport(WARNING,
- errmsg("failed to enable data checksums in \"%s\"", db->dbname));
- found_failed = found;
- continue;
- }
}
FreeDatabaseList(DatabaseList);
- if (found_failed)
- {
- /* Disable checksums on cluster, because we failed */
- SetDataChecksumsOff();
- ereport(ERROR,
- errcode(ERRCODE_INSUFFICIENT_RESOURCES),
- errmsg("data checksums failed to get enabled in all databases, aborting"),
- errhint("The server log might have more information on the cause of the error."));
- }
-
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
return true;
@@ -1421,11 +1375,43 @@ DataChecksumsWorkerShmemInit(void)
ShmemInitStruct("DataChecksumsWorker Data",
DataChecksumsWorkerShmemSize(),
&found);
-
if (!found)
MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
}
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ return found;
+}
+
/*
* BuildDatabaseList
* Compile a list of all currently available databases in the cluster
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 684c1a9b20a..6e917a82072 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -249,6 +249,7 @@ extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
extern bool DataChecksumsNeedWrite(void);
extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgress(void);
extern void SetDataChecksumsOnInProgress(void);
extern void SetDataChecksumsOn(void);
extern void SetDataChecksumsOff(void);
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
index 5601241a803..2428ca6581c 100644
--- a/src/include/postmaster/datachecksum_state.h
+++ b/src/include/postmaster/datachecksum_state.h
@@ -39,6 +39,7 @@ typedef enum
DATACHECKSUMSWORKER_ABORTED,
DATACHECKSUMSWORKER_FAILED,
DATACHECKSUMSWORKER_RETRYDB,
+ DATACHECKSUMSWORKER_DROPDB,
} DataChecksumsWorkerResult;
/* Prototypes for data checksum state manipulation */
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-27 23:30 Heikki Linnakangas <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Heikki Linnakangas @ 2026-03-27 23:30 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
On 28/03/2026 00:03, Daniel Gustafsson wrote:
> The attached rebase contains lots more polish, mostly renaming variable names
> for clarity, tidying up comments and documentation and some smaller bits of
> cleanup like moving more code out of xlog.c.
>
> This version runs all the tests in a normal test-run, with a few of them pared
> down with larger runs gated by PG_TEST_EXTRA. I thinkt the tests are still too
> expensive in the event of getting committed, but it's helpful to have them
> during dev and test. Executing pgbench sometimes fails in CI but I've been
> unable to reproduce that so not entirely sure what is going on there.
>
> Heikki, Andres and Tomas; as you have been reviewing this patchset, what do you
> feel is left for considering this for commit? (Apart from figuring out the CI
> test thing mentioned above which I think is a buildsystem issue.) I think 0001
> could be considered independently of 0002 and is cleanup in it's own right.
+1 for committing 0001 right away.
Some leftover stuff remains, related to the change that it no longer
rebuilds the list of databases:
> + * The DataChecksumsWorker will compile a list of all databases at the start,
> + * and once all are processed will regenerate the list and start over
> + * processing any new entries. Once there are no new entries on the list,
> + * processing will end. The regenerated list is required since databases can
> + * be created concurrently with data checksum processing, using a template
> + * database which has yet to be processed. All databases MUST BE successfully
> + * processed in order for data checksums to be enabled, the only exception are
> + * databases which are dropped before having been processed.
and here:
> +/*
> + * Test to remove an entry from the Databaselist to force re-processing since
> + * not all databases could be processed in the first iteration of the loop.
> + */
> +void
> +dc_dblist(const char *name, const void *private_data, void *arg)
The above talks about re-processing, but that doesn't happen anymore. I
suspect the test that uses this is now obsolete or broken.
> + while (true)
> + {
> + int processed_databases = 0;
> +
> + foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
> + {
> + DataChecksumsWorkerResult result;
> + DataChecksumsWorkerResultEntry *entry;
> + bool found;
> +
> + /*
> + * Check if this database has been processed already, and if so
> + * whether it should be retried or skipped.
> + */
> + entry = (DataChecksumsWorkerResultEntry *) hash_search(ProcessedDatabases, &db->dboid,
> + HASH_FIND, NULL);
> +
I guess this works, but wouldn't it be simpler to remove entries from
DatabaseList when they're successfully processed?
> --- a/src/backend/commands/dbcommands.c
> +++ b/src/backend/commands/dbcommands.c
> @@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
> if (pg_strcasecmp(strategy, "wal_log") == 0)
> dbstrategy = CREATEDB_WAL_LOG;
> else if (pg_strcasecmp(strategy, "file_copy") == 0)
> + {
> + if (DataChecksumsInProgress())
> + ereport(ERROR,
> + errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> + errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
> + strategy));
> dbstrategy = CREATEDB_FILE_COPY;
> + }
> else
> ereport(ERROR,
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
Is this enough? Is it possible that you start CREATE DATABASE in
file_copy mode, and while it's already running but hasn't copied
everything yet, you turn checksums on?
- Heikki
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-03-30 14:43 Daniel Gustafsson <[email protected]>
parent: Heikki Linnakangas <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-03-30 14:43 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
Rebase to keep the CFBot happy since it conflicted with recent commits.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260330-0002-Online-enabling-and-disabling-of-data-chec.patch (223.1K, ../../[email protected]/2-v20260330-0002-Online-enabling-and-disabling-of-data-chec.patch)
download | inline diff:
From 1b7eea98277c2c9a9587f36c4250fa831a3bf238 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 26 Mar 2026 21:55:06 +0100
Subject: [PATCH v20260330 2/2] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calcuated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag to some degree
due to being very time consuming to run.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 12 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 497 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1653 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 18 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/pg_control.h | 6 +
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 264 +++
.../test_checksums/t/007_pgbench_standby.pl | 389 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 178 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 185 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
77 files changed, 5119 insertions(+), 54 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 229f41353eb..bc37fd39873 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index e2db5bcc78c..d9378af0606 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index fd55b9ad23f..e805487344d 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..4a006d057c9 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject delays
+ and re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..50a9a84c865 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..9a62d002002 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,318 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgress(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5254,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5424,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5503,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6580,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6926,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7504,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7566,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7629,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7782,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7928,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8275,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8717,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8861,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8925,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8952,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8992,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9005,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9178,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9243,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..cdbaa0c0337 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point were we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..55ab315c45a 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgress())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 4d8b46adb46..f1b34b60d9b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..8dd9d65b793
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1653 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set of "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the set of conditions to use is
+ * looked up in the checksum_barriers struct. The struct member for the target
+ * state defines which state the backend must currently be in, and which it
+ * must not be in.
+ *
+ * The reason for this explicit checking is to ensure that processing cannot
+ * be started such that it breaks the assumptions of the state machine.
+ *
+ * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
+ * barrier_ne in the below checksum_barriers definition.
+ */
+#define MAX_BARRIER_CONDITIONS 2
+typedef struct ChecksumBarrierCondition
+{
+ /* The target state of the barrier */
+ int target;
+ /* A set of states in which at least one MUST match the current state */
+ int barrier_eq[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_eq set */
+ int barrier_eq_sz;
+ /* A set of states which all MUST NOT match the current state */
+ int barrier_ne[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_ne set */
+ int barrier_ne_sz;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[4] =
+{
+ /*
+ * When disabling checksums, either inprogress state is Ok but checksums
+ * must not be in the enabled state.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ .barrier_eq_sz = 2,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ /* When enabling the current state must be inprogress-on */
+ {
+ .target = PG_DATA_CHECKSUM_VERSION,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+
+ /*
+ * When moving to inprogress-on the current state cannot enabled, but when
+ * moving to inprogress-off the current state must be enabled.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
+ {0}, 0,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsWorkerShmemStruct
+{
+ /*
+ * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * If a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ bool worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsWorkerShmemStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+
+/*
+ * Flag set by the interrupt handler
+ */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/*
+ * Are we enabling data checksums, or disabling them?
+ */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ const ChecksumBarrierCondition *condition = NULL;
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers); i++)
+ {
+ if (checksum_barriers[i].target == target_state)
+ condition = &checksum_barriers[i];
+ }
+ Assert(condition);
+
+ /*
+ * The current state MUST be equal to one of the EQ states defined in this
+ * barrier condition. If the EQ states array is zero then that implies
+ * that the current state can match any state, so fastpath check for that
+ * first.
+ */
+ if (condition->barrier_eq_sz == 0)
+ found = true;
+ else
+ {
+ for (int i = 0; i < condition->barrier_eq_sz; i++)
+ {
+ if (current == condition->barrier_eq[i])
+ found = true;
+ }
+ }
+
+ /*
+ * The current state MUST NOT be equal to any of the NE states defined in
+ * this barrier condition.
+ */
+ for (int i = 0; i < condition->barrier_ne_sz; i++)
+ {
+ if (current == condition->barrier_ne[i])
+ found = false;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumsWorkerShmem->launch_operation = op;
+ DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
+ DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumsWorkerShmem->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /*
+ * As of now we only update the block counter for main forks in order
+ * to not cause too frequent calls. TODO: investigate whether we
+ * should do it more frequent?
+ */
+ if (forkNum == MAIN_FORKNUM)
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ DataChecksumsWorkerShmem->worker_running = true;
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ DataChecksumsWorkerShmem->worker_running = false;
+
+ return DataChecksumsWorkerShmem->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+
+ /*
+ * TODO: how to really handle the worker still running when the
+ * launcher exits?
+ */
+ if (DataChecksumsWorkerShmem->worker_running)
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running"));
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumsWorkerShmem->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->launcher_running = true;
+ DataChecksumsWorkerShmem->operation = operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumsWorkerShmem->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumsWorkerShmemSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsWorkerShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumsWorkerShmemInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsWorkerShmemInit(void)
+{
+ bool found;
+
+ DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsWorkerShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
+ VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumsWorkerShmem->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3fac46c402b..04a3ba04332 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2999,6 +2999,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..d6826709728 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsWorkerShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsWorkerShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..c8ddfc259a3 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -1507,9 +1516,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index f2f8d3ff75f..07fb6bacb40 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 26118661f07..18f77bfb5b4 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 0a862693fcd..c879e1eac40 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cf15597385b..d7133fff599 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 8bb8fa225f6..cdec8a699bb 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -78,7 +78,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..5fde1e847b2 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..146c06cc8ef 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..41a21f43fca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12549,6 +12549,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..fb4fca24991 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..8e21b7d673a
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsWorkerShmemSize(void);
+extern void DataChecksumsWorkerShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..a772bed0a70 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..fe5d30b4349 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ PG_DATA_CHECKSUM_INPROGRESS_ON,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..0c77e16edc9
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 'on');
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..c63d01f3ba7
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..cc4d964989a
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..82c0b1e0f50
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,264 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test is very expensive to run, so by default it runs a pared-down test
+# suite. Setting $extended to 1 will run the full suite and can be done by
+# setting PG_TEST_EXTRA to "checksum_extended".
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+$node->command_ok(
+ [ 'pgbench', '-p', $node->port, '-i', '-s', '100', '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..68a7be7f41e
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,389 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', '100', '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..3553e7b766f
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,178 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', '100', '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..aa0e13bb991
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 'off');
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..6ac715a76f7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,185 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ int new_res = *(int *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index b44aefb545a..1becdaa2e38 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 48c6ae6b0bc..3cd3fc90eba 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -436,6 +436,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -608,6 +610,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -626,6 +629,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsWorkerShmemStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4399,6 +4405,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
[application/octet-stream] v20260330-0001-Add-proper-WAL-record-for-XLOG_CHECKPOINT_.patch (3.9K, ../../[email protected]/3-v20260330-0001-Add-proper-WAL-record-for-XLOG_CHECKPOINT_.patch)
download | inline diff:
From e53b078d88ada9881bfdb458472a7457e0d1323d Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 12 Mar 2026 18:33:02 +0100
Subject: [PATCH v20260330 1/2] Add proper WAL record for XLOG_CHECKPOINT_REDO
XLOG_CHECKPOINT_REDO only contains the wal_level copied straight in
without an encapsulating record structure. While it works, it makes
future uses of XLOG_CHECKPOINT_REDO hard as there is nowhere to put
new data items. This fix this was inspired by the online checksums
patch which adds data to this record, but this change has value on
its own.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/..
---
src/backend/access/rmgrdesc/xlogdesc.c | 6 +++---
src/backend/access/transam/xlog.c | 8 +++++++-
src/backend/postmaster/walsummarizer.c | 5 ++++-
src/include/access/xlog_internal.h | 6 ++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 9044b952117..44194d3ea17 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -163,10 +163,10 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- int wal_level;
+ xl_checkpoint_redo xlrec;
- memcpy(&wal_level, rec, sizeof(int));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(wal_level));
+ memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
+ appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f5c9a34374d..2c1c6f88b74 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7179,9 +7179,15 @@ CreateCheckPoint(int flags)
*/
if (!shutdown)
{
+ xl_checkpoint_redo redo_rec;
+
+ WALInsertLockAcquire();
+ redo_rec.wal_level = wal_level;
+ WALInsertLockRelease();
+
/* Include WAL level in record for WAL summarizer's benefit. */
XLogBeginInsert();
- XLogRegisterData(&wal_level, sizeof(wal_level));
+ XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
/*
diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c
index 0c0670f7da9..a37b3018abf 100644
--- a/src/backend/postmaster/walsummarizer.c
+++ b/src/backend/postmaster/walsummarizer.c
@@ -1435,8 +1435,11 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward)
if (info == XLOG_CHECKPOINT_REDO)
{
+ xl_checkpoint_redo xlrec;
+
/* Payload is wal_level at the time record was written. */
- memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int));
+ memcpy(&xlrec, XLogRecGetData(xlogreader), sizeof(xl_checkpoint_redo));
+ record_wal_level = xlrec.wal_level;
}
else if (info == XLOG_CHECKPOINT_SHUTDOWN)
{
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 1730b07810f..755835d63bf 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -303,6 +303,12 @@ typedef struct xl_end_of_recovery
int wal_level;
} xl_end_of_recovery;
+/* checkpoint redo */
+typedef struct xl_checkpoint_redo
+{
+ int wal_level;
+} xl_checkpoint_redo;
+
/*
* The functions in xloginsert.c construct a chain of XLogRecData structs
* to represent the final WAL record.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e3c1007abdf..48c6ae6b0bc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4398,6 +4398,7 @@ xl_btree_split
xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
+xl_checkpoint_redo
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-01 23:01 Daniel Gustafsson <[email protected]>
parent: Heikki Linnakangas <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-01 23:01 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
Attached is another rebase which adds a note on checksum failure logging for
buffers with ZERO_ON_ERROR, which - apart from being useful information to have
for the user investigating a failure - allows the tests to differentiate
between false positives due to the VM buffer issues discussed in [1].
This patch has now undergone a nontrivial amount of review and testing, have
addressed all review comments and have had no architectural changes in quite
some time. It builds green on CI, and assuming this attached patch also builds
green in CFBot - and no concerns are raised here - I intend to commit this
wihin the next few days ahead of the freeze. The more expensive tests which
are currently enabled will again be moved under PG_TEST_EXTRA to accommodate
for slower machines present in the buildfarm.
--
Daniel Gustafsson
[1] https://postgr.es/m/CAAKRu_bn+e7F4yPFBgFbnP+syJRKyNK092bjD2LKvZW7O4Svag@mail.gmail.com
Attachments:
[application/octet-stream] v20260402-0001-Online-enabling-and-disabling-of-data-chec.patch (225.2K, ../../[email protected]/2-v20260402-0001-Online-enabling-and-disabling-of-data-chec.patch)
download | inline diff:
From 5cda4d01cba077c1080e48969a02b5a2a821e615 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 26 Mar 2026 21:55:06 +0100
Subject: [PATCH v20260402] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calcuated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag to some degree
due to being very time consuming to run.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 12 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 497 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1657 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/buffer/bufmgr.c | 6 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 23 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/pg_control.h | 6 +
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 3 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 265 +++
.../test_checksums/t/007_pgbench_standby.pl | 390 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 179 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 185 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
78 files changed, 5136 insertions(+), 56 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 229f41353eb..bc37fd39873 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b881ae71198 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..7b8ac0fbb32 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..4a006d057c9 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject delays
+ and re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..50a9a84c865 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..9a62d002002 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,318 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgress(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5254,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5424,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5503,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6580,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6926,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7504,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7566,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7629,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7782,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7928,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8275,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8717,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8861,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8925,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8952,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8992,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9005,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9178,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9243,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..9c79dadaacc 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point where we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..55ab315c45a 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgress())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index f2a62489d9c..536aff7ca05 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..9923a18c92c
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1657 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set of "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the set of conditions to use is
+ * looked up in the checksum_barriers struct. The struct member for the target
+ * state defines which state the backend must currently be in, and which it
+ * must not be in.
+ *
+ * The reason for this explicit checking is to ensure that processing cannot
+ * be started such that it breaks the assumptions of the state machine.
+ *
+ * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
+ * barrier_ne in the below checksum_barriers definition.
+ */
+#define MAX_BARRIER_CONDITIONS 2
+typedef struct ChecksumBarrierCondition
+{
+ /* The target state of the barrier */
+ int target;
+ /* A set of states in which at least one MUST match the current state */
+ int barrier_eq[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_eq set */
+ int barrier_eq_sz;
+ /* A set of states which all MUST NOT match the current state */
+ int barrier_ne[MAX_BARRIER_CONDITIONS];
+ /* The number of elements in the barrier_ne set */
+ int barrier_ne_sz;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[4] =
+{
+ /*
+ * When disabling checksums, either inprogress state is Ok but checksums
+ * must not be in the enabled state.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ .barrier_eq_sz = 2,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ /* When enabling the current state must be inprogress-on */
+ {
+ .target = PG_DATA_CHECKSUM_VERSION,
+ .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+
+ /*
+ * When moving to inprogress-on the current state cannot enabled, but when
+ * moving to inprogress-off the current state must be enabled.
+ */
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
+ {0}, 0,
+ .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_ne_sz = 1
+ },
+ {
+ .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
+ .barrier_eq_sz = 1,
+ {0}, 0
+ },
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsWorkerShmemStruct
+{
+ /*
+ * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * If a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ bool worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsWorkerShmemStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+
+/*
+ * Flag set by the interrupt handler
+ */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/*
+ * Are we enabling data checksums, or disabling them?
+ */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ const ChecksumBarrierCondition *condition = NULL;
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers); i++)
+ {
+ if (checksum_barriers[i].target == target_state)
+ condition = &checksum_barriers[i];
+ }
+ Assert(condition);
+
+ /*
+ * The current state MUST be equal to one of the EQ states defined in this
+ * barrier condition. If the EQ states array is zero then that implies
+ * that the current state can match any state, so fastpath check for that
+ * first.
+ */
+ if (condition->barrier_eq_sz == 0)
+ found = true;
+ else
+ {
+ for (int i = 0; i < condition->barrier_eq_sz; i++)
+ {
+ if (current == condition->barrier_eq[i])
+ found = true;
+ }
+ }
+
+ /*
+ * The current state MUST NOT be equal to any of the NE states defined in
+ * this barrier condition.
+ */
+ for (int i = 0; i < condition->barrier_ne_sz; i++)
+ {
+ if (current == condition->barrier_ne[i])
+ found = false;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumsWorkerShmem->launch_operation = op;
+ DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
+ DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumsWorkerShmem->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /*
+ * As of now we only update the block counter for main forks in order
+ * to not cause too frequent calls. TODO: investigate whether we
+ * should do it more frequent?
+ */
+ if (forkNum == MAIN_FORKNUM)
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumsWorkerShmem->worker_running = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumsWorkerShmem->worker_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ return DataChecksumsWorkerShmem->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+
+ /*
+ * TODO: how to really handle the worker still running when the
+ * launcher exits?
+ */
+ if (DataChecksumsWorkerShmem->worker_running)
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running"));
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumsWorkerShmem->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->launcher_running = true;
+ DataChecksumsWorkerShmem->operation = operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumsWorkerShmem->launch_operation != operation)
+ {
+ DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
+ operation = DataChecksumsWorkerShmem->launch_operation;
+ DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
+ DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumsWorkerShmem->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumsWorkerShmem->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumsWorkerShmemSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsWorkerShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumsWorkerShmemInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsWorkerShmemInit(void)
+{
+ bool found;
+
+ DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsWorkerShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
+ VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumsWorkerShmem->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index abf0c97569e..eb4f3eb72d4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2991,6 +2991,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c64570020d..d2018860eca 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8566,6 +8566,12 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
/* the local zero_damaged_pages may differ from the definer's */
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+ /*
+ * If the buffers are marked for zero on error, we want to log that in
+ * case of a checksum failure.
+ */
+ if (flags & READ_BUFFERS_ZERO_ON_ERROR)
+ piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR;
/* Check for garbage data. */
if (!failed)
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..d6826709728 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsWorkerShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsWorkerShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..1fdfda59edd 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("page verification failed, calculated checksum %u but expected %u",
- checksum, p->pd_checksum)));
+ errmsg("page verification failed, calculated checksum %u but expected %u%s",
+ checksum, p->pd_checksum,
+ (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : ""))));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
@@ -1507,9 +1517,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 7727fed3bda..04fe13e64c6 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 577ef5effbb..ecbe2d4b097 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 0a862693fcd..c879e1eac40 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cf15597385b..d7133fff599 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index ce1f6aa30c0..4e0e58b99aa 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -87,7 +87,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..5fde1e847b2 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..146c06cc8ef 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..41a21f43fca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12549,6 +12549,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 04f29748be7..7277c37e779 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..8e21b7d673a
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsWorkerShmemSize(void);
+extern void DataChecksumsWorkerShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..aec539db292 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
@@ -501,6 +501,7 @@ do { \
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)
+#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..fe5d30b4349 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ PG_DATA_CHECKSUM_INPROGRESS_ON,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..c008e95fbff
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 1);
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..c63d01f3ba7
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..cc4d964989a
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..8ed8838a3a5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,265 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test is very expensive to run, so by default it runs a pared-down test
+# suite. Setting $extended to 1 will run the full suite and can be done by
+# setting PG_TEST_EXTRA to "checksum_extended".
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+my $scalefactor = ($extended ? 100 : 1);
+$node->command_ok(
+ [ 'pgbench', '-p', $node->port, '-i', '-s', $scalefactor, '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..b29a01d5edc
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,390 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+my $scalefactor = ($extended ? 100 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..40ef3d3db58
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,179 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+my $scalefactor = ($extended ? 100 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..a1cea91f787
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 1);
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..6ac715a76f7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,185 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ int new_res = *(int *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index b44aefb545a..1becdaa2e38 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5bc517602b1..892117d113a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -436,6 +436,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -608,6 +610,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -626,6 +629,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsWorkerShmemStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4402,6 +4408,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-02 09:27 Heikki Linnakangas <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Heikki Linnakangas @ 2026-04-02 09:27 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
It'd be good to print a LOG message when the checksums state changes, to
have a trail in the log of when checksums were enabled/disabled.
Something like:
LOG: enabling checksums was requested, starting checksum calculation
...
LOG: checksums calculation finished, checksums are now enabled
On 02/04/2026 02:01, Daniel Gustafsson wrote:
> + if (result == DATACHECKSUMSWORKER_FAILED)
> + {
> + /*
> + * Disable checksums on cluster, because we failed one of the
> + * databases and this is an all or nothing process.
> + */
> + SetDataChecksumsOff();
> + ereport(ERROR,
> + errcode(ERRCODE_INSUFFICIENT_RESOURCES),
> + errmsg("data checksums failed to get enabled in all databases, aborting"),
> + errhint("The server log might have more information on the cause of the error."));
> + }
This got me thinking, what happens if the the data checksums launcher
encounters some other error, for example if you SIGTERM it? The system
is left in 'inprogress-on' state, but because the launcher is gone it
will never finish and 'pg_stat_progress_data_checksums' will be empty.
Perhaps launcher_exit() should call SetDataChecksumsOff()?
> /*
> * TODO: how to really handle the worker still running when the
> * launcher exits?
> */
> if (DataChecksumsWorkerShmem->worker_running)
> ereport(LOG,
> errmsg("data checksums launcher exiting while worker is still running"));
That TODO should be addressed somehow.
> + /*
> + * As of now we only update the block counter for main forks in order
> + * to not cause too frequent calls. TODO: investigate whether we
> + * should do it more frequent?
> + */
> + if (forkNum == MAIN_FORKNUM)
> + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
> + (blknum + 1));
We're updating it for every block in the main fork, but not at all for
other forks? What a bizarre way of avoiding too frequent calls :-). I
think you could just call this on every page,
pgstat_progress_update_param() is supposed to be very fast. For
comparison, e.g. index build calls it for every tuple.
> +/*
> + * Configuration of conditions which must match when absorbing a procsignal
> + * barrier during data checksum enable/disable operations. A single function
> + * is used for absorbing all barriers, and the set of conditions to use is
> + * looked up in the checksum_barriers struct. The struct member for the target
> + * state defines which state the backend must currently be in, and which it
> + * must not be in.
> + *
> + * The reason for this explicit checking is to ensure that processing cannot
> + * be started such that it breaks the assumptions of the state machine.
> + *
> + * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
> + * barrier_ne in the below checksum_barriers definition.
> + */
> +#define MAX_BARRIER_CONDITIONS 2
> +typedef struct ChecksumBarrierCondition
> +{
> + /* The target state of the barrier */
> + int target;
> + /* A set of states in which at least one MUST match the current state */
> + int barrier_eq[MAX_BARRIER_CONDITIONS];
> + /* The number of elements in the barrier_eq set */
> + int barrier_eq_sz;
> + /* A set of states which all MUST NOT match the current state */
> + int barrier_ne[MAX_BARRIER_CONDITIONS];
> + /* The number of elements in the barrier_ne set */
> + int barrier_ne_sz;
> +} ChecksumBarrierCondition;
> +
> +static const ChecksumBarrierCondition checksum_barriers[4] =
> +{
> + /*
> + * When disabling checksums, either inprogress state is Ok but checksums
> + * must not be in the enabled state.
> + */
> + {
> + .target = PG_DATA_CHECKSUM_OFF,
> + .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
> + .barrier_eq_sz = 2,
> + .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
> + .barrier_ne_sz = 1
> + },
> + /* When enabling the current state must be inprogress-on */
> + {
> + .target = PG_DATA_CHECKSUM_VERSION,
> + .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
> + .barrier_eq_sz = 1,
> + {0}, 0
> + },
> +
> + /*
> + * When moving to inprogress-on the current state cannot enabled, but when
> + * moving to inprogress-off the current state must be enabled.
> + */
> + {
> + .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
> + {0}, 0,
> + .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
> + .barrier_ne_sz = 1
> + },
> + {
> + .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
> + .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
> + .barrier_eq_sz = 1,
> + {0}, 0
> + },
> +};
I find this to still be a pretty complicated and unclear way of
representing the allowed transitions. There are only 16 possible
transitions, and only 6 of them are allowed. How about listing the
allowed ones directly:
/* Allowed transitions: from, to */
{
/*
* Disabling checksums: If checksums are currently enabled,
* disabling must go through the 'inprogress-off' state.
*/
{PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
/*
* If checksums are in the process of being enabled, but are
* not yet being verified, we can abort by going back to 'off'
* state.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
/*
* Enabling checksums must normally go through the 'inprogress-on'
* state.
*/
{PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
/*
* If checksums are being disabled but all backends are still
* computing checksums, we can go straight back to 'on'
*/
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
}
> +/*
> + * Signaling between backends calling pg_enable/disable_data_checksums, the
> + * checksums launcher process, and the checksums worker process.
> + *
> + * This struct is protected by DataChecksumsWorkerLock
> + */
> +typedef struct DataChecksumsWorkerShmemStruct
> +{
> + /*
> + * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
> + * launcher what the target state is.
> + */
> + DataChecksumsWorkerOperation launch_operation;
> + int launch_cost_delay;
> + int launch_cost_limit;
The naming feels a little weird with this struct. It's called
"DataChecksumsWorkerShmemStruct", but it's also accessed by the backends
calling pg_enable/disable_data_checksums(). And
"DataChecksumsWorkerOperation" is not accessed by workers at all. Or I
guess the "operation" global variable is used in
DataChecksumsWorkerMain(), but it's always set to ENABLE_DATACHECKSUMS
in the worker. Do you need the "operation" global variable at all?
> +void
> +SetDataChecksumsOn(void)
> +{
> + uint64 barrier;
> +
> + Assert(ControlFile != NULL);
> +
> + SpinLockAcquire(&XLogCtl->info_lck);
> +
> + /*
> + * The only allowed state transition to "on" is from "inprogress-on" since
> + * that state ensures that all pages will have data checksums written.
> + */
> + if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
> + {
> + SpinLockRelease(&XLogCtl->info_lck);
> + elog(PANIC, "checksums not in \"inprogress-on\" mode");
> + }
> +
> + SpinLockRelease(&XLogCtl->info_lck);
The PANIC seems a little harsh, you haven't done anything destructive
here. It's unexpected for this to be called in any other state, so this
is a "can't happen" scenario, but I don't think we usually PANIC on those.
> + <para>
> + If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
> + specified, the process is throttled using the same principles as
> + <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
> + </para>
Ugh, yet another place where we expose the "cost delay/limit" throttling
mechanism. I agree it's good to be consistent here and use the same
method we use for vacuum, I just wish we had something more user-friendly..
Grammar / spelling:
> + * state will also be set of "off".
> + * When moving to inprogress-on the current state cannot enabled, but when
> + * If a worker process currently running? This is set by the worker
> + * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
there is no "pg_verify_data_checksums" function.
"calcuated" in commit message
- Heikki
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-02 18:48 Daniel Gustafsson <[email protected]>
parent: Heikki Linnakangas <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-02 18:48 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 2 Apr 2026, at 11:27, Heikki Linnakangas <[email protected]> wrote:
Thanks for looking!
> It'd be good to print a LOG message when the checksums state changes, to have a trail in the log of when checksums were enabled/disabled. Something like:
>
> LOG: enabling checksums was requested, starting checksum calculation
> ...
> LOG: checksums calculation finished, checksums are now enabled
Done.
> On 02/04/2026 02:01, Daniel Gustafsson wrote:
>> + if (result == DATACHECKSUMSWORKER_FAILED)
>> + {
>> + /*
>> + * Disable checksums on cluster, because we failed one of the
>> + * databases and this is an all or nothing process.
>> + */
>> + SetDataChecksumsOff();
>> + ereport(ERROR,
>> + errcode(ERRCODE_INSUFFICIENT_RESOURCES),
>> + errmsg("data checksums failed to get enabled in all databases, aborting"),
>> + errhint("The server log might have more information on the cause of the error."));
>> + }
>
> This got me thinking, what happens if the the data checksums launcher encounters some other error, for example if you SIGTERM it? The system is left in 'inprogress-on' state, but because the launcher is gone it will never finish and 'pg_stat_progress_data_checksums' will be empty.
>
> Perhaps launcher_exit() should call SetDataChecksumsOff()?
I made launcher_exit call SetDataChecksumsOff in case the current state is
in-progress.
>> /*
>> * TODO: how to really handle the worker still running when the
>> * launcher exits?
>> */
>> if (DataChecksumsWorkerShmem->worker_running)
>> ereport(LOG,
>> errmsg("data checksums launcher exiting while worker is still running"));
>
> That TODO should be addressed somehow.
Since processing cannot reach a new state if the launcher goes away, I resorted
to signalling the worker to die in this case.
>> + /*
>> + * As of now we only update the block counter for main forks in order
>> + * to not cause too frequent calls. TODO: investigate whether we
>> + * should do it more frequent?
>> + */
>> + if (forkNum == MAIN_FORKNUM)
>> + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
>> + (blknum + 1));
>
> We're updating it for every block in the main fork, but not at all for other forks? What a bizarre way of avoiding too frequent calls :-). I think you could just call this on every page, pgstat_progress_update_param() is supposed to be very fast. For comparison, e.g. index build calls it for every tuple.
Ok, done.
>> +static const ChecksumBarrierCondition checksum_barriers[4] =
>> +{
>> + /*
>> + * When disabling checksums, either inprogress state is Ok but checksums
>> + * must not be in the enabled state.
>> + */
>> + {
>> + .target = PG_DATA_CHECKSUM_OFF,
>> + .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
>> + .barrier_eq_sz = 2,
<..snip..>
>> + .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
>> + .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
>> + .barrier_eq_sz = 1,
>> + {0}, 0
>> + },
>> +};
>
> I find this to still be a pretty complicated and unclear way of representing the allowed transitions. There are only 16 possible transitions, and only 6 of them are allowed. How about listing the allowed ones directly:
Fair enough, I've replaced it with a simpler to/from struct and the for loop
then checks for a struct member which contains the current as .from and target
as .to.
>> +/*
>> + * Signaling between backends calling pg_enable/disable_data_checksums, the
>> + * checksums launcher process, and the checksums worker process.
>> + *
>> + * This struct is protected by DataChecksumsWorkerLock
>> + */
>> +typedef struct DataChecksumsWorkerShmemStruct
>> +{
>> + /*
>> + * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
>> + * launcher what the target state is.
>> + */
>> + DataChecksumsWorkerOperation launch_operation;
>> + int launch_cost_delay;
>> + int launch_cost_limit;
>
> The naming feels a little weird with this struct. It's called "DataChecksumsWorkerShmemStruct", but it's also accessed by the backends calling pg_enable/disable_data_checksums(). And "DataChecksumsWorkerOperation" is not accessed by workers at all. Or I guess the "operation" global variable is used in DataChecksumsWorkerMain(), but it's always set to ENABLE_DATACHECKSUMS in the worker. Do you need the "operation" global variable at all?
I admittedly hadn't realized but you are quite right. I renamed the structure
to DataChecksumsState instead which seems to fit better. The operation
variable is used to check against the launch_operation such that a call to
disable checksums while an enabling is running will abort processing
gracefully.
>> + if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
>> + {
>> + SpinLockRelease(&XLogCtl->info_lck);
>> + elog(PANIC, "checksums not in \"inprogress-on\" mode");
>> + }
>> +
>> + SpinLockRelease(&XLogCtl->info_lck);
>
> The PANIC seems a little harsh, you haven't done anything destructive here. It's unexpected for this to be called in any other state, so this is a "can't happen" scenario, but I don't think we usually PANIC on those.
That's a good point, I've replaced with a WARNING and a SetDataChecksumsOff()
call to move to a safe state.
>> + <para>
>> + If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
>> + specified, the process is throttled using the same principles as
>> + <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
>> + </para>
>
> Ugh, yet another place where we expose the "cost delay/limit" throttling mechanism. I agree it's good to be consistent here and use the same method we use for vacuum, I just wish we had something more user-friendly..
Agreed on both counts.
> Grammar / spelling:
>
>> + * state will also be set of "off".
>
>> + * When moving to inprogress-on the current state cannot enabled, but when
>
>> + * If a worker process currently running? This is set by the worker
>
>> + * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
>
> there is no "pg_verify_data_checksums" function.
Fixed.
> "calcuated" in commit message
Fixed.
Thank you so much the continued reviewing support. The attached rebase
contains the above fixes as well a few small cleanups detected by Github
CoPilot review. This version removes more than it adds, a trend which has been
going on for quite some versions, and the patch is now just north of 5000 loc
compared to the 5300 it was not long ago. The changes from the previous are
attached as a text file as well for easier teasing apart what was new.
--
Daniel Gustafsson
commit a2b37f504a88b8d479af9eb07e6fcb42c591abf4
Author: Daniel Gustafsson <[email protected]>
Date: Thu Apr 2 20:09:57 2026 +0200
fixupå
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9a62d002002..1ce81d00d85 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4781,12 +4781,17 @@ SetDataChecksumsOn(void)
/*
* The only allowed state transition to "on" is from "inprogress-on" since
- * that state ensures that all pages will have data checksums written.
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
*/
if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
{
SpinLockRelease(&XLogCtl->info_lck);
- elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
}
SpinLockRelease(&XLogCtl->info_lck);
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 9923a18c92c..754edc5c746 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -96,7 +96,7 @@
* If processing was halted by the cluster shutting down (due to a crash or
* intentional restart), the controlfile state "inprogress-on" will be observed
* on system startup and all backends will be placed in Bd. The controlfile
- * state will also be set of "off".
+ * state will also be set to "off".
*
* Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
* DataChecksumsLauncher. When all backends have acknowledged the barrier then
@@ -223,69 +223,43 @@
/*
* Configuration of conditions which must match when absorbing a procsignal
* barrier during data checksum enable/disable operations. A single function
- * is used for absorbing all barriers, and the set of conditions to use is
- * looked up in the checksum_barriers struct. The struct member for the target
- * state defines which state the backend must currently be in, and which it
- * must not be in.
- *
- * The reason for this explicit checking is to ensure that processing cannot
- * be started such that it breaks the assumptions of the state machine.
- *
- * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
- * barrier_ne in the below checksum_barriers definition.
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
*/
-#define MAX_BARRIER_CONDITIONS 2
typedef struct ChecksumBarrierCondition
{
- /* The target state of the barrier */
- int target;
- /* A set of states in which at least one MUST match the current state */
- int barrier_eq[MAX_BARRIER_CONDITIONS];
- /* The number of elements in the barrier_eq set */
- int barrier_eq_sz;
- /* A set of states which all MUST NOT match the current state */
- int barrier_ne[MAX_BARRIER_CONDITIONS];
- /* The number of elements in the barrier_ne set */
- int barrier_ne_sz;
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[4] =
+static const ChecksumBarrierCondition checksum_barriers[6] =
{
/*
- * When disabling checksums, either inprogress state is Ok but checksums
- * must not be in the enabled state.
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
*/
- {
- .target = PG_DATA_CHECKSUM_OFF,
- .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
- .barrier_eq_sz = 2,
- .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
- .barrier_ne_sz = 1
- },
- /* When enabling the current state must be inprogress-on */
- {
- .target = PG_DATA_CHECKSUM_VERSION,
- .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
- .barrier_eq_sz = 1,
- {0}, 0
- },
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
/*
- * When moving to inprogress-on the current state cannot enabled, but when
- * moving to inprogress-off the current state must be enabled.
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
*/
- {
- .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
- {0}, 0,
- .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
- .barrier_ne_sz = 1
- },
- {
- .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
- .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
- .barrier_eq_sz = 1,
- {0}, 0
- },
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
};
/*
@@ -294,10 +268,10 @@ static const ChecksumBarrierCondition checksum_barriers[4] =
*
* This struct is protected by DataChecksumsWorkerLock
*/
-typedef struct DataChecksumsWorkerShmemStruct
+typedef struct DataChecksumsStateStruct
{
/*
- * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
* launcher what the target state is.
*/
DataChecksumsWorkerOperation launch_operation;
@@ -311,10 +285,10 @@ typedef struct DataChecksumsWorkerShmemStruct
bool launcher_running;
/*
- * If a worker process currently running? This is set by the worker
+ * Is a worker process currently running? This is set by the worker
* launcher when it starts waiting for a worker process to finish.
*/
- bool worker_running;
+ int worker_running;
/*
* These fields indicate the target state that the launcher is currently
@@ -348,10 +322,10 @@ typedef struct DataChecksumsWorkerShmemStruct
* catalogs
*/
bool process_shared_catalogs;
-} DataChecksumsWorkerShmemStruct;
+} DataChecksumsStateStruct;
/* Shared memory segment for datachecksumsworker */
-static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+static DataChecksumsStateStruct *DataChecksumState;
typedef struct DataChecksumsWorkerDatabase
{
@@ -359,21 +333,16 @@ typedef struct DataChecksumsWorkerDatabase
char *dbname;
} DataChecksumsWorkerDatabase;
-
-/*
- * Flag set by the interrupt handler
- */
+/* Flag set by the interrupt handler */
static volatile sig_atomic_t abort_requested = false;
/*
- * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
* If we have, we need to clear it before exiting!
*/
static volatile sig_atomic_t launcher_running = false;
-/*
- * Are we enabling data checksums, or disabling them?
- */
+/* Are we enabling data checksums, or disabling them? */
static DataChecksumsWorkerOperation operation;
/* Prototypes */
@@ -435,7 +404,6 @@ EmitAndWaitDataChecksumsBarrier(uint32 state)
bool
AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
{
- const ChecksumBarrierCondition *condition = NULL;
uint32 target_state;
int current = data_checksums;
bool found = false;
@@ -485,38 +453,10 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
* a condition would be a grave programmer error as the states are a
* discrete set.
*/
- for (int i = 0; i < lengthof(checksum_barriers); i++)
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
{
- if (checksum_barriers[i].target == target_state)
- condition = &checksum_barriers[i];
- }
- Assert(condition);
-
- /*
- * The current state MUST be equal to one of the EQ states defined in this
- * barrier condition. If the EQ states array is zero then that implies
- * that the current state can match any state, so fastpath check for that
- * first.
- */
- if (condition->barrier_eq_sz == 0)
- found = true;
- else
- {
- for (int i = 0; i < condition->barrier_eq_sz; i++)
- {
- if (current == condition->barrier_eq[i])
- found = true;
- }
- }
-
- /*
- * The current state MUST NOT be equal to any of the NE states defined in
- * this barrier condition.
- */
- for (int i = 0; i < condition->barrier_ne_sz; i++)
- {
- if (current == condition->barrier_ne[i])
- found = false;
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
}
/*
@@ -614,14 +554,14 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
/* Store the desired state in shared memory */
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- DataChecksumsWorkerShmem->launch_operation = op;
- DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
- DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
/* Is the launcher already running? If so, what is it doing? */
- launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ launcher_running = DataChecksumState->launcher_running;
if (launcher_running)
- launcher_running_op = DataChecksumsWorkerShmem->operation;
+ launcher_running_op = DataChecksumState->operation;
LWLockRelease(DataChecksumsWorkerLock);
@@ -733,21 +673,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
*/
Assert(operation == ENABLE_DATACHECKSUMS);
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
abort_requested = true;
LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
return false;
- /*
- * As of now we only update the block counter for main forks in order
- * to not cause too frequent calls. TODO: investigate whether we
- * should do it more frequent?
- */
- if (forkNum == MAIN_FORKNUM)
- pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
- (blknum + 1));
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
/*
* Processing is re-using the vacuum cost delay for process
@@ -826,7 +761,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
pid_t pid;
char activity[NAMEDATALEN + 64];
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
memset(&bgw, 0, sizeof(bgw));
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
@@ -890,8 +825,9 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
errmsg("initiating data checksum processing in database \"%s\"",
db->dbname));
+ /* Save the pid of the worker so we can signal it later */
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- DataChecksumsWorkerShmem->worker_running = true;
+ DataChecksumState->worker_running = pid;
LWLockRelease(DataChecksumsWorkerLock);
snprintf(activity, sizeof(activity) - 1,
@@ -906,17 +842,17 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
db->dbname),
errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
- if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
ereport(LOG,
errmsg("data checksums processing was aborted in database \"%s\"",
db->dbname));
pgstat_report_activity(STATE_IDLE, NULL);
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- DataChecksumsWorkerShmem->worker_running = false;
+ DataChecksumState->worker_running = InvalidPid;
LWLockRelease(DataChecksumsWorkerLock);
- return DataChecksumsWorkerShmem->success;
+ return DataChecksumState->success;
}
/*
@@ -936,17 +872,23 @@ launcher_exit(int code, Datum arg)
{
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
launcher_running = false;
- DataChecksumsWorkerShmem->launcher_running = false;
+ DataChecksumState->launcher_running = false;
- /*
- * TODO: how to really handle the worker still running when the
- * launcher exits?
- */
- if (DataChecksumsWorkerShmem->worker_running)
+ if (DataChecksumState->worker_running != InvalidPid)
+ {
ereport(LOG,
- errmsg("data checksums launcher exiting while worker is still running"));
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_running, SIGTERM);
+ }
LWLockRelease(DataChecksumsWorkerLock);
}
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgress())
+ SetDataChecksumsOff();
}
/*
@@ -1027,7 +969,7 @@ WaitForAllTransactionsToFinish(void)
CHECK_FOR_INTERRUPTS();
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumsWorkerShmem->launch_operation != operation)
+ if (DataChecksumState->launch_operation != operation)
abort_requested = true;
LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
@@ -1068,7 +1010,7 @@ DataChecksumsWorkerLauncherMain(Datum arg)
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- if (DataChecksumsWorkerShmem->launcher_running)
+ if (DataChecksumState->launcher_running)
{
ereport(LOG,
errmsg("background worker \"datachecksums launcher\" already running, exiting"));
@@ -1082,11 +1024,11 @@ DataChecksumsWorkerLauncherMain(Datum arg)
/* Initialize a connection to shared catalogs only */
BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
- operation = DataChecksumsWorkerShmem->launch_operation;
- DataChecksumsWorkerShmem->launcher_running = true;
- DataChecksumsWorkerShmem->operation = operation;
- DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
- DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
LWLockRelease(DataChecksumsWorkerLock);
/*
@@ -1109,6 +1051,9 @@ again:
if (DataChecksumsNeedVerify())
goto done;
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
/*
* Set the state to inprogress-on and wait on the procsignal barrier.
*/
@@ -1127,7 +1072,7 @@ again:
* failure, so restart processing instead.
*/
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- if (DataChecksumsWorkerShmem->launch_operation != operation)
+ if (DataChecksumState->launch_operation != operation)
{
LWLockRelease(DataChecksumsWorkerLock);
goto done;
@@ -1143,12 +1088,20 @@ again:
* order to instruct backends to validate checksums on reading.
*/
SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
}
else if (operation == DISABLE_DATACHECKSUMS)
{
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
}
else
Assert(false);
@@ -1168,12 +1121,12 @@ done:
* again.
*/
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- if (DataChecksumsWorkerShmem->launch_operation != operation)
+ if (DataChecksumState->launch_operation != operation)
{
- DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
- operation = DataChecksumsWorkerShmem->launch_operation;
- DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
- DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
LWLockRelease(DataChecksumsWorkerLock);
goto again;
}
@@ -1182,7 +1135,7 @@ done:
pgstat_progress_end_command();
launcher_running = false;
- DataChecksumsWorkerShmem->launcher_running = false;
+ DataChecksumState->launcher_running = false;
LWLockRelease(DataChecksumsWorkerLock);
}
@@ -1201,7 +1154,7 @@ ProcessAllDatabases(void)
int cumulative_total = 0;
/* Set up so first run processes shared catalogs, not once in every db */
- DataChecksumsWorkerShmem->process_shared_catalogs = true;
+ DataChecksumState->process_shared_catalogs = true;
/* Get a list of all databases to process */
WaitForAllTransactionsToFinish();
@@ -1270,7 +1223,7 @@ ProcessAllDatabases(void)
* When one database has completed, it will have done shared catalogs
* so we don't have to process them again.
*/
- DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ DataChecksumState->process_shared_catalogs = false;
}
FreeDatabaseList(DatabaseList);
@@ -1281,35 +1234,35 @@ ProcessAllDatabases(void)
}
/*
- * DataChecksumsWorkerShmemSize
+ * DataChecksumStateSize
* Compute required space for datachecksumsworker-related shared memory
*/
Size
-DataChecksumsWorkerShmemSize(void)
+DataChecksumsShmemSize(void)
{
Size size;
- size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = sizeof(DataChecksumsStateStruct);
size = MAXALIGN(size);
return size;
}
/*
- * DataChecksumsWorkerShmemInit
+ * DataChecksumStateInit
* Allocate and initialize datachecksumsworker-related shared memory
*/
void
-DataChecksumsWorkerShmemInit(void)
+DataChecksumsShmemInit(void)
{
bool found;
- DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ DataChecksumState = (DataChecksumsStateStruct *)
ShmemInitStruct("DataChecksumsWorker Data",
- DataChecksumsWorkerShmemSize(),
+ DataChecksumsShmemSize(),
&found);
if (!found)
- MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
}
/*
@@ -1528,9 +1481,9 @@ DataChecksumsWorkerMain(Datum arg)
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
*/
- Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
- VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
- VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
VacuumCostActive = (VacuumCostDelay > 0);
VacuumCostBalance = 0;
VacuumCostPageHit = 0;
@@ -1543,7 +1496,7 @@ DataChecksumsWorkerMain(Datum arg)
strategy = GetAccessStrategy(BAS_VACUUM);
RelationList = BuildRelationList(false,
- DataChecksumsWorkerShmem->process_shared_catalogs);
+ DataChecksumState->process_shared_catalogs);
/* Update the total number of relations to be processed in this DB. */
{
@@ -1579,7 +1532,7 @@ DataChecksumsWorkerMain(Datum arg)
if (aborted)
{
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
ereport(DEBUG1,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1633,14 +1586,14 @@ DataChecksumsWorkerMain(Datum arg)
WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ aborted = DataChecksumState->launch_operation != operation;
LWLockRelease(DataChecksumsWorkerLock);
CHECK_FOR_INTERRUPTS();
if (aborted || abort_requested)
{
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
ereport(LOG,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1653,5 +1606,5 @@ DataChecksumsWorkerMain(Datum arg)
/* worker done */
pgstat_progress_end_command();
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d2018860eca..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8566,6 +8566,7 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
/* the local zero_damaged_pages may differ from the definer's */
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+
/*
* If the buffers are marked for zero on error, we want to log that in
* case of a checksum failure.
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d6826709728..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -143,7 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
- size = add_size(size, DataChecksumsWorkerShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -312,7 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
- DataChecksumsWorkerShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
index 8e21b7d673a..343494edcc8 100644
--- a/src/include/postmaster/datachecksum_state.h
+++ b/src/include/postmaster/datachecksum_state.h
@@ -18,8 +18,8 @@
#include "storage/procsignal.h"
/* Shared memory */
-extern Size DataChecksumsWorkerShmemSize(void);
-extern void DataChecksumsWorkerShmemInit(void);
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
/* Possible operations the Datachecksumsworker can perform */
typedef enum DataChecksumsWorkerOperation
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
index c63d01f3ba7..bab59be82bd 100644
--- a/src/test/modules/test_checksums/t/002_restarts.pl
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -100,7 +100,7 @@ is($result, '9999', 'ensure checksummed pages can be read back');
$result = $node->poll_query_until(
'postgres',
- "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
'0');
is($result, 1, 'await datachecksums worker/launcher termination');
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index cc4d964989a..6b016925651 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -89,7 +89,7 @@ is($result, '19998', 'ensure we can safely read all data with checksums');
$result = $node_primary->poll_query_until(
'postgres',
- "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
'0');
is($result, 1, 'await datachecksums worker/launcher termination');
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
index 6ac715a76f7..edded415931 100644
--- a/src/test/modules/test_checksums/test_checksums.c
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -22,7 +22,6 @@
PG_MODULE_MAGIC;
extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
-extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
@@ -115,7 +114,7 @@ void
dc_modify_db_result(const char *name, const void *private_data, void *arg)
{
DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
- int new_res = *(int *) private_data;
+ uint32 new_res = *(uint32 *) private_data;
*res = new_res;
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 892117d113a..61132ee3377 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -631,7 +631,7 @@ DSMRegistryEntry
DWORD
DataChecksumsWorkerDatabase
DataChecksumsWorkerResult
-DataChecksumsWorkerShmemStruct
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
Attachments:
[application/octet-stream] v20260402_2-0001-Online-enabling-and-disabling-of-data-ch.patch (223.6K, ../../[email protected]/2-v20260402_2-0001-Online-enabling-and-disabling-of-data-ch.patch)
download | inline diff:
From 9d9d8c99db028ffebf476e90596d363ab2ecb039 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 2 Apr 2026 20:37:35 +0200
Subject: [PATCH v20260402_2] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calculated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag to some degree
due to being very time consuming to run.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 12 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 502 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1610 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/buffer/bufmgr.c | 7 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 23 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/pg_control.h | 6 +
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 3 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 265 +++
.../test_checksums/t/007_pgbench_standby.pl | 390 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 179 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 184 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
78 files changed, 5094 insertions(+), 56 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fdb77df0fdb..a5b16625c3c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b881ae71198 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..7b8ac0fbb32 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..4a006d057c9 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject delays
+ and re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..50a9a84c865 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..1ce81d00d85 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,323 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgress(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5259,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5429,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5508,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6585,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6931,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7509,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7571,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7634,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7787,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7933,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8280,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8722,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8866,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8930,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8957,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8997,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9010,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9183,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9248,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..9c79dadaacc 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point where we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..55ab315c45a 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgress())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index f2a62489d9c..536aff7ca05 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..754edc5c746
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1610 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set to "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
+ */
+typedef struct ChecksumBarrierCondition
+{
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[6] =
+{
+ /*
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
+ */
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsStateStruct
+{
+ /*
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * Is a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ int worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsStateStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsStateStruct *DataChecksumState;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+/* Flag set by the interrupt handler */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/* Are we enabling data checksums, or disabling them? */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
+ {
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumState->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumState->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ /* Save the pid of the worker so we can signal it later */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_running = pid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_running = InvalidPid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ return DataChecksumState->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+
+ if (DataChecksumState->worker_running != InvalidPid)
+ {
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_running, SIGTERM);
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgress())
+ SetDataChecksumsOff();
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumState->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumState->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumState->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumStateSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsStateStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumStateInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsShmemInit(void)
+{
+ bool found;
+
+ DataChecksumState = (DataChecksumsStateStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumState->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumState->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index abf0c97569e..eb4f3eb72d4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2991,6 +2991,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c64570020d..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8567,6 +8567,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+ /*
+ * If the buffers are marked for zero on error, we want to log that in
+ * case of a checksum failure.
+ */
+ if (flags & READ_BUFFERS_ZERO_ON_ERROR)
+ piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR;
+
/* Check for garbage data. */
if (!failed)
{
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..1fdfda59edd 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("page verification failed, calculated checksum %u but expected %u",
- checksum, p->pd_checksum)));
+ errmsg("page verification failed, calculated checksum %u but expected %u%s",
+ checksum, p->pd_checksum,
+ (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : ""))));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
@@ -1507,9 +1517,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 7727fed3bda..04fe13e64c6 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 577ef5effbb..ecbe2d4b097 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index e556b8844d8..eb7e3dfa460 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2c5e98d1d4d..59363fed1a2 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index ce1f6aa30c0..4e0e58b99aa 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -87,7 +87,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..5fde1e847b2 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..146c06cc8ef 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..41a21f43fca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12549,6 +12549,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 04f29748be7..7277c37e779 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..343494edcc8
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..aec539db292 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
@@ -501,6 +501,7 @@ do { \
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)
+#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..fe5d30b4349 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ PG_DATA_CHECKSUM_INPROGRESS_ON,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..c008e95fbff
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 1);
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..bab59be82bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..6b016925651
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..8ed8838a3a5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,265 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test is very expensive to run, so by default it runs a pared-down test
+# suite. Setting $extended to 1 will run the full suite and can be done by
+# setting PG_TEST_EXTRA to "checksum_extended".
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+my $scalefactor = ($extended ? 100 : 1);
+$node->command_ok(
+ [ 'pgbench', '-p', $node->port, '-i', '-s', $scalefactor, '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..b29a01d5edc
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,390 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+my $scalefactor = ($extended ? 100 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..40ef3d3db58
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,179 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/)
+{
+ $extended = 1;
+}
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+my $scalefactor = ($extended ? 100 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..a1cea91f787
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 1);
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..edded415931
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,184 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ uint32 new_res = *(uint32 *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index f8dc732e66e..54e6b646e8f 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5bc517602b1..61132ee3377 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -436,6 +436,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -608,6 +610,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -626,6 +629,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4402,6 +4408,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
[text/plain] v20260402_2_diff.txt (25.3K, ../../[email protected]/3-v20260402_2_diff.txt)
download | inline diff:
commit a2b37f504a88b8d479af9eb07e6fcb42c591abf4
Author: Daniel Gustafsson <[email protected]>
Date: Thu Apr 2 20:09:57 2026 +0200
fixupå
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9a62d002002..1ce81d00d85 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4781,12 +4781,17 @@ SetDataChecksumsOn(void)
/*
* The only allowed state transition to "on" is from "inprogress-on" since
- * that state ensures that all pages will have data checksums written.
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
*/
if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
{
SpinLockRelease(&XLogCtl->info_lck);
- elog(PANIC, "checksums not in \"inprogress-on\" mode");
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
}
SpinLockRelease(&XLogCtl->info_lck);
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 9923a18c92c..754edc5c746 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -96,7 +96,7 @@
* If processing was halted by the cluster shutting down (due to a crash or
* intentional restart), the controlfile state "inprogress-on" will be observed
* on system startup and all backends will be placed in Bd. The controlfile
- * state will also be set of "off".
+ * state will also be set to "off".
*
* Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
* DataChecksumsLauncher. When all backends have acknowledged the barrier then
@@ -223,69 +223,43 @@
/*
* Configuration of conditions which must match when absorbing a procsignal
* barrier during data checksum enable/disable operations. A single function
- * is used for absorbing all barriers, and the set of conditions to use is
- * looked up in the checksum_barriers struct. The struct member for the target
- * state defines which state the backend must currently be in, and which it
- * must not be in.
- *
- * The reason for this explicit checking is to ensure that processing cannot
- * be started such that it breaks the assumptions of the state machine.
- *
- * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
- * barrier_ne in the below checksum_barriers definition.
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
*/
-#define MAX_BARRIER_CONDITIONS 2
typedef struct ChecksumBarrierCondition
{
- /* The target state of the barrier */
- int target;
- /* A set of states in which at least one MUST match the current state */
- int barrier_eq[MAX_BARRIER_CONDITIONS];
- /* The number of elements in the barrier_eq set */
- int barrier_eq_sz;
- /* A set of states which all MUST NOT match the current state */
- int barrier_ne[MAX_BARRIER_CONDITIONS];
- /* The number of elements in the barrier_ne set */
- int barrier_ne_sz;
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[4] =
+static const ChecksumBarrierCondition checksum_barriers[6] =
{
/*
- * When disabling checksums, either inprogress state is Ok but checksums
- * must not be in the enabled state.
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
*/
- {
- .target = PG_DATA_CHECKSUM_OFF,
- .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
- .barrier_eq_sz = 2,
- .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
- .barrier_ne_sz = 1
- },
- /* When enabling the current state must be inprogress-on */
- {
- .target = PG_DATA_CHECKSUM_VERSION,
- .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
- .barrier_eq_sz = 1,
- {0}, 0
- },
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
/*
- * When moving to inprogress-on the current state cannot enabled, but when
- * moving to inprogress-off the current state must be enabled.
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
*/
- {
- .target = PG_DATA_CHECKSUM_INPROGRESS_ON,
- {0}, 0,
- .barrier_ne = {PG_DATA_CHECKSUM_VERSION},
- .barrier_ne_sz = 1
- },
- {
- .target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
- .barrier_eq = {PG_DATA_CHECKSUM_VERSION},
- .barrier_eq_sz = 1,
- {0}, 0
- },
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
};
/*
@@ -294,10 +268,10 @@ static const ChecksumBarrierCondition checksum_barriers[4] =
*
* This struct is protected by DataChecksumsWorkerLock
*/
-typedef struct DataChecksumsWorkerShmemStruct
+typedef struct DataChecksumsStateStruct
{
/*
- * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
* launcher what the target state is.
*/
DataChecksumsWorkerOperation launch_operation;
@@ -311,10 +285,10 @@ typedef struct DataChecksumsWorkerShmemStruct
bool launcher_running;
/*
- * If a worker process currently running? This is set by the worker
+ * Is a worker process currently running? This is set by the worker
* launcher when it starts waiting for a worker process to finish.
*/
- bool worker_running;
+ int worker_running;
/*
* These fields indicate the target state that the launcher is currently
@@ -348,10 +322,10 @@ typedef struct DataChecksumsWorkerShmemStruct
* catalogs
*/
bool process_shared_catalogs;
-} DataChecksumsWorkerShmemStruct;
+} DataChecksumsStateStruct;
/* Shared memory segment for datachecksumsworker */
-static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+static DataChecksumsStateStruct *DataChecksumState;
typedef struct DataChecksumsWorkerDatabase
{
@@ -359,21 +333,16 @@ typedef struct DataChecksumsWorkerDatabase
char *dbname;
} DataChecksumsWorkerDatabase;
-
-/*
- * Flag set by the interrupt handler
- */
+/* Flag set by the interrupt handler */
static volatile sig_atomic_t abort_requested = false;
/*
- * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
* If we have, we need to clear it before exiting!
*/
static volatile sig_atomic_t launcher_running = false;
-/*
- * Are we enabling data checksums, or disabling them?
- */
+/* Are we enabling data checksums, or disabling them? */
static DataChecksumsWorkerOperation operation;
/* Prototypes */
@@ -435,7 +404,6 @@ EmitAndWaitDataChecksumsBarrier(uint32 state)
bool
AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
{
- const ChecksumBarrierCondition *condition = NULL;
uint32 target_state;
int current = data_checksums;
bool found = false;
@@ -485,38 +453,10 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
* a condition would be a grave programmer error as the states are a
* discrete set.
*/
- for (int i = 0; i < lengthof(checksum_barriers); i++)
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
{
- if (checksum_barriers[i].target == target_state)
- condition = &checksum_barriers[i];
- }
- Assert(condition);
-
- /*
- * The current state MUST be equal to one of the EQ states defined in this
- * barrier condition. If the EQ states array is zero then that implies
- * that the current state can match any state, so fastpath check for that
- * first.
- */
- if (condition->barrier_eq_sz == 0)
- found = true;
- else
- {
- for (int i = 0; i < condition->barrier_eq_sz; i++)
- {
- if (current == condition->barrier_eq[i])
- found = true;
- }
- }
-
- /*
- * The current state MUST NOT be equal to any of the NE states defined in
- * this barrier condition.
- */
- for (int i = 0; i < condition->barrier_ne_sz; i++)
- {
- if (current == condition->barrier_ne[i])
- found = false;
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
}
/*
@@ -614,14 +554,14 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
/* Store the desired state in shared memory */
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- DataChecksumsWorkerShmem->launch_operation = op;
- DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
- DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
/* Is the launcher already running? If so, what is it doing? */
- launcher_running = DataChecksumsWorkerShmem->launcher_running;
+ launcher_running = DataChecksumState->launcher_running;
if (launcher_running)
- launcher_running_op = DataChecksumsWorkerShmem->operation;
+ launcher_running_op = DataChecksumState->operation;
LWLockRelease(DataChecksumsWorkerLock);
@@ -733,21 +673,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
*/
Assert(operation == ENABLE_DATACHECKSUMS);
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
abort_requested = true;
LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
return false;
- /*
- * As of now we only update the block counter for main forks in order
- * to not cause too frequent calls. TODO: investigate whether we
- * should do it more frequent?
- */
- if (forkNum == MAIN_FORKNUM)
- pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
- (blknum + 1));
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
/*
* Processing is re-using the vacuum cost delay for process
@@ -826,7 +761,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
pid_t pid;
char activity[NAMEDATALEN + 64];
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
memset(&bgw, 0, sizeof(bgw));
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
@@ -890,8 +825,9 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
errmsg("initiating data checksum processing in database \"%s\"",
db->dbname));
+ /* Save the pid of the worker so we can signal it later */
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- DataChecksumsWorkerShmem->worker_running = true;
+ DataChecksumState->worker_running = pid;
LWLockRelease(DataChecksumsWorkerLock);
snprintf(activity, sizeof(activity) - 1,
@@ -906,17 +842,17 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
db->dbname),
errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
- if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
ereport(LOG,
errmsg("data checksums processing was aborted in database \"%s\"",
db->dbname));
pgstat_report_activity(STATE_IDLE, NULL);
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- DataChecksumsWorkerShmem->worker_running = false;
+ DataChecksumState->worker_running = InvalidPid;
LWLockRelease(DataChecksumsWorkerLock);
- return DataChecksumsWorkerShmem->success;
+ return DataChecksumState->success;
}
/*
@@ -936,17 +872,23 @@ launcher_exit(int code, Datum arg)
{
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
launcher_running = false;
- DataChecksumsWorkerShmem->launcher_running = false;
+ DataChecksumState->launcher_running = false;
- /*
- * TODO: how to really handle the worker still running when the
- * launcher exits?
- */
- if (DataChecksumsWorkerShmem->worker_running)
+ if (DataChecksumState->worker_running != InvalidPid)
+ {
ereport(LOG,
- errmsg("data checksums launcher exiting while worker is still running"));
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_running, SIGTERM);
+ }
LWLockRelease(DataChecksumsWorkerLock);
}
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgress())
+ SetDataChecksumsOff();
}
/*
@@ -1027,7 +969,7 @@ WaitForAllTransactionsToFinish(void)
CHECK_FOR_INTERRUPTS();
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumsWorkerShmem->launch_operation != operation)
+ if (DataChecksumState->launch_operation != operation)
abort_requested = true;
LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
@@ -1068,7 +1010,7 @@ DataChecksumsWorkerLauncherMain(Datum arg)
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- if (DataChecksumsWorkerShmem->launcher_running)
+ if (DataChecksumState->launcher_running)
{
ereport(LOG,
errmsg("background worker \"datachecksums launcher\" already running, exiting"));
@@ -1082,11 +1024,11 @@ DataChecksumsWorkerLauncherMain(Datum arg)
/* Initialize a connection to shared catalogs only */
BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
- operation = DataChecksumsWorkerShmem->launch_operation;
- DataChecksumsWorkerShmem->launcher_running = true;
- DataChecksumsWorkerShmem->operation = operation;
- DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
- DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
LWLockRelease(DataChecksumsWorkerLock);
/*
@@ -1109,6 +1051,9 @@ again:
if (DataChecksumsNeedVerify())
goto done;
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
/*
* Set the state to inprogress-on and wait on the procsignal barrier.
*/
@@ -1127,7 +1072,7 @@ again:
* failure, so restart processing instead.
*/
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- if (DataChecksumsWorkerShmem->launch_operation != operation)
+ if (DataChecksumState->launch_operation != operation)
{
LWLockRelease(DataChecksumsWorkerLock);
goto done;
@@ -1143,12 +1088,20 @@ again:
* order to instruct backends to validate checksums on reading.
*/
SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
}
else if (operation == DISABLE_DATACHECKSUMS)
{
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
}
else
Assert(false);
@@ -1168,12 +1121,12 @@ done:
* again.
*/
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- if (DataChecksumsWorkerShmem->launch_operation != operation)
+ if (DataChecksumState->launch_operation != operation)
{
- DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
- operation = DataChecksumsWorkerShmem->launch_operation;
- DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
- DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
LWLockRelease(DataChecksumsWorkerLock);
goto again;
}
@@ -1182,7 +1135,7 @@ done:
pgstat_progress_end_command();
launcher_running = false;
- DataChecksumsWorkerShmem->launcher_running = false;
+ DataChecksumState->launcher_running = false;
LWLockRelease(DataChecksumsWorkerLock);
}
@@ -1201,7 +1154,7 @@ ProcessAllDatabases(void)
int cumulative_total = 0;
/* Set up so first run processes shared catalogs, not once in every db */
- DataChecksumsWorkerShmem->process_shared_catalogs = true;
+ DataChecksumState->process_shared_catalogs = true;
/* Get a list of all databases to process */
WaitForAllTransactionsToFinish();
@@ -1270,7 +1223,7 @@ ProcessAllDatabases(void)
* When one database has completed, it will have done shared catalogs
* so we don't have to process them again.
*/
- DataChecksumsWorkerShmem->process_shared_catalogs = false;
+ DataChecksumState->process_shared_catalogs = false;
}
FreeDatabaseList(DatabaseList);
@@ -1281,35 +1234,35 @@ ProcessAllDatabases(void)
}
/*
- * DataChecksumsWorkerShmemSize
+ * DataChecksumStateSize
* Compute required space for datachecksumsworker-related shared memory
*/
Size
-DataChecksumsWorkerShmemSize(void)
+DataChecksumsShmemSize(void)
{
Size size;
- size = sizeof(DataChecksumsWorkerShmemStruct);
+ size = sizeof(DataChecksumsStateStruct);
size = MAXALIGN(size);
return size;
}
/*
- * DataChecksumsWorkerShmemInit
+ * DataChecksumStateInit
* Allocate and initialize datachecksumsworker-related shared memory
*/
void
-DataChecksumsWorkerShmemInit(void)
+DataChecksumsShmemInit(void)
{
bool found;
- DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+ DataChecksumState = (DataChecksumsStateStruct *)
ShmemInitStruct("DataChecksumsWorker Data",
- DataChecksumsWorkerShmemSize(),
+ DataChecksumsShmemSize(),
&found);
if (!found)
- MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
}
/*
@@ -1528,9 +1481,9 @@ DataChecksumsWorkerMain(Datum arg)
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
*/
- Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
- VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
- VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
VacuumCostActive = (VacuumCostDelay > 0);
VacuumCostBalance = 0;
VacuumCostPageHit = 0;
@@ -1543,7 +1496,7 @@ DataChecksumsWorkerMain(Datum arg)
strategy = GetAccessStrategy(BAS_VACUUM);
RelationList = BuildRelationList(false,
- DataChecksumsWorkerShmem->process_shared_catalogs);
+ DataChecksumState->process_shared_catalogs);
/* Update the total number of relations to be processed in this DB. */
{
@@ -1579,7 +1532,7 @@ DataChecksumsWorkerMain(Datum arg)
if (aborted)
{
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
ereport(DEBUG1,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1633,14 +1586,14 @@ DataChecksumsWorkerMain(Datum arg)
WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+ aborted = DataChecksumState->launch_operation != operation;
LWLockRelease(DataChecksumsWorkerLock);
CHECK_FOR_INTERRUPTS();
if (aborted || abort_requested)
{
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
ereport(LOG,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1653,5 +1606,5 @@ DataChecksumsWorkerMain(Datum arg)
/* worker done */
pgstat_progress_end_command();
- DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d2018860eca..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8566,6 +8566,7 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
/* the local zero_damaged_pages may differ from the definer's */
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+
/*
* If the buffers are marked for zero on error, we want to log that in
* case of a checksum failure.
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d6826709728..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -143,7 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
- size = add_size(size, DataChecksumsWorkerShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -312,7 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
- DataChecksumsWorkerShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
index 8e21b7d673a..343494edcc8 100644
--- a/src/include/postmaster/datachecksum_state.h
+++ b/src/include/postmaster/datachecksum_state.h
@@ -18,8 +18,8 @@
#include "storage/procsignal.h"
/* Shared memory */
-extern Size DataChecksumsWorkerShmemSize(void);
-extern void DataChecksumsWorkerShmemInit(void);
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
/* Possible operations the Datachecksumsworker can perform */
typedef enum DataChecksumsWorkerOperation
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
index c63d01f3ba7..bab59be82bd 100644
--- a/src/test/modules/test_checksums/t/002_restarts.pl
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -100,7 +100,7 @@ is($result, '9999', 'ensure checksummed pages can be read back');
$result = $node->poll_query_until(
'postgres',
- "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
'0');
is($result, 1, 'await datachecksums worker/launcher termination');
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index cc4d964989a..6b016925651 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -89,7 +89,7 @@ is($result, '19998', 'ensure we can safely read all data with checksums');
$result = $node_primary->poll_query_until(
'postgres',
- "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
'0');
is($result, 1, 'await datachecksums worker/launcher termination');
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
index 6ac715a76f7..edded415931 100644
--- a/src/test/modules/test_checksums/test_checksums.c
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -22,7 +22,6 @@
PG_MODULE_MAGIC;
extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
-extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
@@ -115,7 +114,7 @@ void
dc_modify_db_result(const char *name, const void *private_data, void *arg)
{
DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
- int new_res = *(int *) private_data;
+ uint32 new_res = *(uint32 *) private_data;
*res = new_res;
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 892117d113a..61132ee3377 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -631,7 +631,7 @@ DSMRegistryEntry
DWORD
DataChecksumsWorkerDatabase
DataChecksumsWorkerResult
-DataChecksumsWorkerShmemStruct
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 15:33 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-03 15:33 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
The attached rebase with a PG_CONTROL_VERSION bump is what I have staged for
later tonight, submitting here to have the (hopefully) final patch archived as
well as another CFBot run.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260403-0001-Online-enabling-and-disabling-of-data-chec.patch (225.2K, ../../[email protected]/2-v20260403-0001-Online-enabling-and-disabling-of-data-chec.patch)
download | inline diff:
From b327a450f0e116853dee027adcb98a748f49f23c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 3 Apr 2026 17:00:32 +0200
Subject: [PATCH v20260403] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calculated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag due to being
very expensive to run. Two levels of PG_TEST_EXTRA flags exist
to turn on a subset of the expensive tests, or the full suite of
multiple runs.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 14 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 502 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1610 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/buffer/bufmgr.c | 7 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 23 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/pg_control.h | 8 +-
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 3 +-
src/include/storage/checksum.h | 14 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 275 +++
.../test_checksums/t/007_pgbench_standby.pl | 400 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 189 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 184 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
78 files changed, 5127 insertions(+), 57 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fdb77df0fdb..a5b16625c3c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b881ae71198 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..7b8ac0fbb32 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..c74941bfbf2 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,20 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum</literal>, <literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject faults
+ to cause re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ <literal>checksum_extended</literal> is an extended version with
+ longer runtime, injected random delays and larger datasets.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..50a9a84c865 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/bufpage.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..1ce81d00d85 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,323 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgress(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5259,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5429,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5508,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6585,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6931,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7509,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7571,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7634,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7787,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7933,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8280,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8722,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8866,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8930,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8957,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8997,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9010,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9183,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9248,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..9c79dadaacc 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point where we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..55ab315c45a 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgress())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index f2a62489d9c..536aff7ca05 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..754edc5c746
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1610 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set to "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
+ */
+typedef struct ChecksumBarrierCondition
+{
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[6] =
+{
+ /*
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
+ */
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsStateStruct
+{
+ /*
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * Is a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ int worker_running;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsStateStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsStateStruct *DataChecksumState;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+/* Flag set by the interrupt handler */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/* Are we enabling data checksums, or disabling them? */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
+ {
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumState->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumState->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ /* Save the pid of the worker so we can signal it later */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_running = pid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_running = InvalidPid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ return DataChecksumState->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+
+ if (DataChecksumState->worker_running != InvalidPid)
+ {
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_running, SIGTERM);
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgress())
+ SetDataChecksumsOff();
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumState->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumState->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumState->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumStateSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsStateStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumStateInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsShmemInit(void)
+{
+ bool found;
+
+ DataChecksumState = (DataChecksumsStateStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumState->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumState->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index abf0c97569e..eb4f3eb72d4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2991,6 +2991,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c64570020d..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8567,6 +8567,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+ /*
+ * If the buffers are marked for zero on error, we want to log that in
+ * case of a checksum failure.
+ */
+ if (flags & READ_BUFFERS_ZERO_ON_ERROR)
+ piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR;
+
/* Check for garbage data. */
if (!failed)
{
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..1fdfda59edd 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("page verification failed, calculated checksum %u but expected %u",
- checksum, p->pd_checksum)));
+ errmsg("page verification failed, calculated checksum %u but expected %u%s",
+ checksum, p->pd_checksum,
+ (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : ""))));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
@@ -1507,9 +1517,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 7727fed3bda..04fe13e64c6 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 577ef5effbb..ecbe2d4b097 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index e556b8844d8..eb7e3dfa460 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2c5e98d1d4d..59363fed1a2 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/bufpage.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index a268f0f1dd0..7dd1c3dd63e 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -79,7 +79,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..5fde1e847b2 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..80b3a730e03 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
/* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION 1901
+#define PG_CONTROL_VERSION 1902
/* Nonce key length, see below */
#define MOCK_AUTH_NONCE_LEN 32
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acf16254b21..bd177aebfcb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12558,6 +12558,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 04f29748be7..7277c37e779 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..343494edcc8
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..aec539db292 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
#include "access/xlogdefs.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/off.h"
/* GUC variable */
@@ -230,7 +231,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
@@ -501,6 +501,7 @@ do { \
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)
+#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..fe5d30b4349 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF,
+ PG_DATA_CHECKSUM_INPROGRESS_ON,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..c008e95fbff
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 1);
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..bab59be82bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..6b016925651
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..0ab5b04b931
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,275 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+my $scalefactor = ($extended ? 10 : 1);
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..b0d40d24005
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,400 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..b9b89f414ab
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,189 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..a1cea91f787
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 1);
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..b087a3b4664
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,184 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ uint32 new_res = *(uint32 *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index f8dc732e66e..54e6b646e8f 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91b1225da82..ad999aa48dd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -438,6 +438,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -610,6 +612,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -628,6 +631,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4405,6 +4411,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 16:00 Heikki Linnakangas <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Heikki Linnakangas @ 2026-04-03 16:00 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
On 03/04/2026 18:33, Daniel Gustafsson wrote:
> The attached rebase with a PG_CONTROL_VERSION bump is what I have staged for
> later tonight, submitting here to have the (hopefully) final patch archived as
> well as another CFBot run.
A few more small comments, I'm sorry about drip-feeding these:
> +/*
> + * launcher_exit
> + *
> + * Internal routine for cleaning up state when the launcher process exits. We
> + * need to clean up the abort flag to ensure that processing started again if
> + * it was previously aborted (note: started again, *not* restarted from where
> + * it left off).
> + */
> +static void
> +launcher_exit(int code, Datum arg)
> +{
> + abort_requested = false;
> +
> + if (launcher_running)
> + {
> + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
> + launcher_running = false;
> + DataChecksumState->launcher_running = false;
> +
> + if (DataChecksumState->worker_running != InvalidPid)
> + {
> + ereport(LOG,
> + errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
> + kill(DataChecksumState->worker_running, SIGTERM);
> + }
> + LWLockRelease(DataChecksumsWorkerLock);
> + }
> +
> + /*
> + * If the launcher is exiting before data checksums are enabled then set
> + * the state to off since processing cannot be resumed.
> + */
> + if (DataChecksumsInProgress())
> + SetDataChecksumsOff();
> +}
Is there still a race condition if the launcher is killed, it gets here,
sends SIGTERM to the worker process, but before the worker process has
exited, the user calls pg_enable_data_checksums() again and a new
launcher is started? What happens?
> + /*
> + * Is a worker process currently running? This is set by the worker
> + * launcher when it starts waiting for a worker process to finish.
> + */
> + int worker_running;
'worker_running' sounds like a boolean, but it's actually a PID.
Especially when 'launcher_running' really is a boolean. Maybe rename to
'worker_pid' or 'worker_running_pid' or 'running_worker_pid' or something.
> +bool
> +DataChecksumsInProgress(void)
> +{
> + return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
> +}
Perhaps this should be DataChecksumsInProgressOn()? I saw the caller in
launcher_exit() first, and had to look up the implementation to check if
it returns true for 'inprogress-off' state.
> diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
> index ff417d5ae3e..fe5d30b4349 100644
> --- a/src/include/storage/checksum.h
> +++ b/src/include/storage/checksum.h
> @@ -15,6 +15,20 @@
>
> #include "storage/block.h"
>
> +/*
> + * Checksum state 0 is used for when data checksums are disabled (OFF).
> + * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
> + * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
> + * that data checksums are enabled.
> + */
> +typedef enum ChecksumStateType
> +{
> + PG_DATA_CHECKSUM_OFF = 0,
> + PG_DATA_CHECKSUM_VERSION,
> + PG_DATA_CHECKSUM_INPROGRESS_OFF,
> + PG_DATA_CHECKSUM_INPROGRESS_ON,
> +} ChecksumStateType;
> +
> /*
> * Compute the checksum for a Postgres page. The page must be aligned on a
> * 4-byte boundary.
It'd be good to mention that this value is stored in the control file,
so changing it needs a catversion bump. Also it's important that
PG_DATA_CHECKSUM_VERSION = 1, for backwards-compatibility in pg_upgrade.
I'd suggest assigning explicit values 1, 2, 3, 4 for each of the enum
constants, to emphasize that they values are fixed.
There's an #include "storage/bufpage.h" in
src/bin/pg_upgrade/controldata.c and
src/backend/access/rmgrdesc/xlogdesc.c. I think they should both include
"storage/checksum.h" directly instead. And "bufpage.h" probably doesn't
need to include "storage/checksum.h".
- Heikki
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 17:14 Daniel Gustafsson <[email protected]>
parent: Heikki Linnakangas <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-03 17:14 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 3 Apr 2026, at 18:00, Heikki Linnakangas <[email protected]> wrote:
>
> On 03/04/2026 18:33, Daniel Gustafsson wrote:
>> The attached rebase with a PG_CONTROL_VERSION bump is what I have staged for
>> later tonight, submitting here to have the (hopefully) final patch archived as
>> well as another CFBot run.
>
> A few more small comments, I'm sorry about drip-feeding these:
Not at all, thanks for looking! I'm sure there will be a few more things once
there is BF coverage of the patch as well.
>> +/*
>> + * launcher_exit
>> + *
>> + * Internal routine for cleaning up state when the launcher process exits. We
>> + * need to clean up the abort flag to ensure that processing started again if
>> + * it was previously aborted (note: started again, *not* restarted from where
>> + * it left off).
>> + */
>> +static void
>> +launcher_exit(int code, Datum arg)
>> +{
>> + abort_requested = false;
>> +
>> + if (launcher_running)
>> + {
>> + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
>> + launcher_running = false;
>> + DataChecksumState->launcher_running = false;
>> +
>> + if (DataChecksumState->worker_running != InvalidPid)
>> + {
>> + ereport(LOG,
>> + errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
>> + kill(DataChecksumState->worker_running, SIGTERM);
>> + }
>> + LWLockRelease(DataChecksumsWorkerLock);
>> + }
>> +
>> + /*
>> + * If the launcher is exiting before data checksums are enabled then set
>> + * the state to off since processing cannot be resumed.
>> + */
>> + if (DataChecksumsInProgress())
>> + SetDataChecksumsOff();
>> +}
>
> Is there still a race condition if the launcher is killed, it gets here, sends SIGTERM to the worker process, but before the worker process has exited, the user calls pg_enable_data_checksums() again and a new launcher is started? What happens?
If the new launcher sets the state to inprogress-on before the old one checks
for that in launcher_exit, it would disable checksums, and the new launcher
would fail in the next state transitiom as the state is incorrect. The
solution would be to hold the DataChecksumsWorkerLock exclusively during
launcher_exit to ensure the old one can exit before the new one starts. Done
in the attached.
>> + /*
>> + * Is a worker process currently running? This is set by the worker
>> + * launcher when it starts waiting for a worker process to finish.
>> + */
>> + int worker_running;
>
> 'worker_running' sounds like a boolean, but it's actually a PID. Especially when 'launcher_running' really is a boolean. Maybe rename to 'worker_pid' or 'worker_running_pid' or 'running_worker_pid' or something.
Renamed to worker_pid.
>> +bool
>> +DataChecksumsInProgress(void)
>> +{
>> + return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
>> +}
>
> Perhaps this should be DataChecksumsInProgressOn()? I saw the caller in launcher_exit() first, and had to look up the implementation to check if it returns true for 'inprogress-off' state.
Fixed.
>> diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
>> index ff417d5ae3e..fe5d30b4349 100644
>> --- a/src/include/storage/checksum.h
>> +++ b/src/include/storage/checksum.h
>> @@ -15,6 +15,20 @@
>> #include "storage/block.h"
>> +/*
>> + * Checksum state 0 is used for when data checksums are disabled (OFF).
>> + * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
>> + * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
>> + * that data checksums are enabled.
>> + */
>> +typedef enum ChecksumStateType
>> +{
>> + PG_DATA_CHECKSUM_OFF = 0,
>> + PG_DATA_CHECKSUM_VERSION,
>> + PG_DATA_CHECKSUM_INPROGRESS_OFF,
>> + PG_DATA_CHECKSUM_INPROGRESS_ON,
>> +} ChecksumStateType;
>> +
>> /*
>> * Compute the checksum for a Postgres page. The page must be aligned on a
>> * 4-byte boundary.
>
> It'd be good to mention that this value is stored in the control file, so changing it needs a catversion bump. Also it's important that PG_DATA_CHECKSUM_VERSION = 1, for backwards-compatibility in pg_upgrade. I'd suggest assigning explicit values 1, 2, 3, 4 for each of the enum constants, to emphasize that they values are fixed.
Fixed.
> There's an #include "storage/bufpage.h" in src/bin/pg_upgrade/controldata.c and src/backend/access/rmgrdesc/xlogdesc.c. I think they should both include "storage/checksum.h" directly instead. And "bufpage.h" probably doesn't need to include "storage/checksum.h".
Fixed, and validated with headerscheck. Doing this required adding checksum.h
to bootstrap/bootstrap.c which seems very correct as it needs to know about the
state enum.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260403_2-0001-Online-enabling-and-disabling-of-data-ch.patch (226.0K, ../../[email protected]/2-v20260403_2-0001-Online-enabling-and-disabling-of-data-ch.patch)
download | inline diff:
From 77c36295ad412a9544e70c68f37ccb795539b92d Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 3 Apr 2026 17:00:32 +0200
Subject: [PATCH v20260403_2] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calculated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag due to being
very expensive to run. Two levels of PG_TEST_EXTRA flags exist
to turn on a subset of the expensive tests, or the full suite of
multiple runs.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 14 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 502 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/bootstrap/bootstrap.c | 1 +
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1611 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/buffer/bufmgr.c | 7 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 23 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/pg_control.h | 8 +-
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 16 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 275 +++
.../test_checksums/t/007_pgbench_standby.pl | 400 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 189 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 184 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
80 files changed, 5131 insertions(+), 58 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fdb77df0fdb..a5b16625c3c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b881ae71198 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..7b8ac0fbb32 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..c74941bfbf2 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,20 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum</literal>, <literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject faults
+ to cause re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ <literal>checksum_extended</literal> is an extended version with
+ longer runtime, injected random delays and larger datasets.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..2468a7d2578 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/checksum.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..9e8999bbb61 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,323 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgressOn(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5259,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5429,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5508,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6585,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6931,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7509,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7571,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7634,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7787,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7933,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8280,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8722,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8866,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8930,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8957,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8997,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9010,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9183,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9248,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..9c79dadaacc 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point where we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 38ef683d4c7..c52c0a6023d 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -35,6 +35,7 @@
#include "port/pg_getopt_ctx.h"
#include "postmaster/postmaster.h"
#include "storage/bufpage.h"
+#include "storage/checksum.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/proc.h"
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..f0819d15ab7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgressOn())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index f2a62489d9c..536aff7ca05 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..10ac7db0d01
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1611 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set to "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
+ */
+typedef struct ChecksumBarrierCondition
+{
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[6] =
+{
+ /*
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
+ */
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsStateStruct
+{
+ /*
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * Is a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ int worker_pid;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsStateStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsStateStruct *DataChecksumState;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+/* Flag set by the interrupt handler */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/* Are we enabling data checksums, or disabling them? */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
+ {
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumState->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumState->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ /* Save the pid of the worker so we can signal it later */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_pid = pid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_pid = InvalidPid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ return DataChecksumState->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (launcher_running)
+ {
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+
+ if (DataChecksumState->worker_pid != InvalidPid)
+ {
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_pid, SIGTERM);
+ }
+ }
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgressOn())
+ SetDataChecksumsOff();
+
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumState->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumState->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumState->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumStateSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsStateStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumStateInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsShmemInit(void)
+{
+ bool found;
+
+ DataChecksumState = (DataChecksumsStateStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumState->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumState->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index abf0c97569e..eb4f3eb72d4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2991,6 +2991,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c64570020d..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8567,6 +8567,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+ /*
+ * If the buffers are marked for zero on error, we want to log that in
+ * case of a checksum failure.
+ */
+ if (flags & READ_BUFFERS_ZERO_ON_ERROR)
+ piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR;
+
/* Check for garbage data. */
if (!failed)
{
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..1fdfda59edd 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("page verification failed, calculated checksum %u but expected %u",
- checksum, p->pd_checksum)));
+ errmsg("page verification failed, calculated checksum %u but expected %u%s",
+ checksum, p->pd_checksum,
+ (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : ""))));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
@@ -1507,9 +1517,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 7727fed3bda..04fe13e64c6 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 577ef5effbb..ecbe2d4b097 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index e556b8844d8..eb7e3dfa460 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2c5e98d1d4d..59363fed1a2 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..79053d22dcc 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/checksum.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index a268f0f1dd0..7dd1c3dd63e 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -79,7 +79,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..4af38e74ce4 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgressOn(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index b1c5afc15df..582bb2e2058 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202604021
+#define CATALOG_VERSION_NO 202604031
#endif
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..80b3a730e03 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
/* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION 1901
+#define PG_CONTROL_VERSION 1902
/* Nonce key length, see below */
#define MOCK_AUTH_NONCE_LEN 32
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acf16254b21..bd177aebfcb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12558,6 +12558,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 04f29748be7..7277c37e779 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..343494edcc8
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..634e1e49ee5 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -230,7 +230,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
@@ -501,6 +500,7 @@ do { \
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)
+#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..3b1440c0c95 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,22 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled. The ChecksumStateType is stored in
+ * pg_control so changing requires a catversion bump, and the values cannot
+ * be reordered. New states must be added at the end.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION = 1,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF = 2,
+ PG_DATA_CHECKSUM_INPROGRESS_ON = 3,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..c008e95fbff
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 1);
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..bab59be82bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..6b016925651
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..0ab5b04b931
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,275 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+my $scalefactor = ($extended ? 10 : 1);
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..b0d40d24005
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,400 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..b9b89f414ab
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,189 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..a1cea91f787
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 1);
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..b087a3b4664
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,184 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ uint32 new_res = *(uint32 *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index f8dc732e66e..54e6b646e8f 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91b1225da82..ad999aa48dd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -438,6 +438,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -610,6 +612,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -628,6 +631,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4405,6 +4411,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 17:56 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-03 17:56 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
CFBot claims it needs a rebase which I'm not sure why (I can't reproduce a
rebase error), but here is a rebase anyways.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260403_3-0001-Online-enabling-and-disabling-of-data-ch.patch (226.0K, ../../[email protected]/2-v20260403_3-0001-Online-enabling-and-disabling-of-data-ch.patch)
download | inline diff:
From c21b5311611f586b1bad546e806516c2c8420733 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 3 Apr 2026 17:00:32 +0200
Subject: [PATCH v20260403_3] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calculated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag due to being
very expensive to run. Two levels of PG_TEST_EXTRA flags exist
to turn on a subset of the expensive tests, or the full suite of
multiple runs.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 14 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 502 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/bootstrap/bootstrap.c | 1 +
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1611 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/buffer/bufmgr.c | 7 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 23 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/pg_control.h | 8 +-
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 16 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 275 +++
.../test_checksums/t/007_pgbench_standby.pl | 400 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 189 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 184 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
80 files changed, 5131 insertions(+), 58 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 422ba304982..d3fea738ca3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b881ae71198 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..7b8ac0fbb32 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..c74941bfbf2 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,20 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum</literal>, <literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject faults
+ to cause re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ <literal>checksum_extended</literal> is an extended version with
+ longer runtime, injected random delays and larger datasets.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..2468a7d2578 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/checksum.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..9e8999bbb61 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,323 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgressOn(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5259,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5429,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5508,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6585,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6931,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7509,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7571,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7634,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7787,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7933,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8280,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8722,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8866,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8930,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8957,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8997,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9010,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9183,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9248,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..9c79dadaacc 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point where we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 38ef683d4c7..c52c0a6023d 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -35,6 +35,7 @@
#include "port/pg_getopt_ctx.h"
#include "postmaster/postmaster.h"
#include "storage/bufpage.h"
+#include "storage/checksum.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/proc.h"
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..f0819d15ab7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgressOn())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index f2a62489d9c..536aff7ca05 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..10ac7db0d01
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1611 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set to "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
+ */
+typedef struct ChecksumBarrierCondition
+{
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[6] =
+{
+ /*
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
+ */
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsStateStruct
+{
+ /*
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * Is a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ int worker_pid;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsStateStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsStateStruct *DataChecksumState;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+/* Flag set by the interrupt handler */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/* Are we enabling data checksums, or disabling them? */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
+ {
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumState->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumState->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ /* Save the pid of the worker so we can signal it later */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_pid = pid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_pid = InvalidPid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ return DataChecksumState->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (launcher_running)
+ {
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+
+ if (DataChecksumState->worker_pid != InvalidPid)
+ {
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_pid, SIGTERM);
+ }
+ }
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgressOn())
+ SetDataChecksumsOff();
+
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumState->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumState->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumState->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumStateSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsStateStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumStateInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsShmemInit(void)
+{
+ bool found;
+
+ DataChecksumState = (DataChecksumsStateStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumState->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumState->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index abf0c97569e..eb4f3eb72d4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2991,6 +2991,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c64570020d..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8567,6 +8567,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+ /*
+ * If the buffers are marked for zero on error, we want to log that in
+ * case of a checksum failure.
+ */
+ if (flags & READ_BUFFERS_ZERO_ON_ERROR)
+ piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR;
+
/* Check for garbage data. */
if (!failed)
{
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..1fdfda59edd 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("page verification failed, calculated checksum %u but expected %u",
- checksum, p->pd_checksum)));
+ errmsg("page verification failed, calculated checksum %u but expected %u%s",
+ checksum, p->pd_checksum,
+ (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : ""))));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
@@ -1507,9 +1517,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 7727fed3bda..04fe13e64c6 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 783a7400464..6f074013aa9 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index fc0900efe5f..a315c4ab8ab 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c8194c27aa7..6d0337853e0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..79053d22dcc 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/checksum.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index a268f0f1dd0..7dd1c3dd63e 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -79,7 +79,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..4af38e74ce4 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgressOn(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index b1c5afc15df..582bb2e2058 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202604021
+#define CATALOG_VERSION_NO 202604031
#endif
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..80b3a730e03 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
/* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION 1901
+#define PG_CONTROL_VERSION 1902
/* Nonce key length, see below */
#define MOCK_AUTH_NONCE_LEN 32
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acf16254b21..bd177aebfcb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12558,6 +12558,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 04f29748be7..7277c37e779 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..343494edcc8
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..634e1e49ee5 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -230,7 +230,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
@@ -501,6 +500,7 @@ do { \
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)
+#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..3b1440c0c95 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,22 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled. The ChecksumStateType is stored in
+ * pg_control so changing requires a catversion bump, and the values cannot
+ * be reordered. New states must be added at the end.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION = 1,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF = 2,
+ PG_DATA_CHECKSUM_INPROGRESS_ON = 3,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..c008e95fbff
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 1);
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..bab59be82bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..6b016925651
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..0ab5b04b931
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,275 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+my $scalefactor = ($extended ? 10 : 1);
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..b0d40d24005
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,400 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..b9b89f414ab
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,189 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..a1cea91f787
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 1);
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..b087a3b4664
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,184 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ uint32 new_res = *(uint32 *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index f8dc732e66e..54e6b646e8f 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91b1225da82..ad999aa48dd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -438,6 +438,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -610,6 +612,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -628,6 +631,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4405,6 +4411,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 19:29 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-03 19:29 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 3 Apr 2026, at 19:56, Daniel Gustafsson <[email protected]> wrote:
Come to think of it, a cleaner solution is to just postpone updating the
launcher_running flag until it actually not running anymore. Done in the
attached which has done several CI runs now.
> CFBot claims it needs a rebase which I'm not sure why (I can't reproduce a
> rebase error), but here is a rebase anyways.
Turns out it's applying on 57706799186 rather than HEAD, and the catversion
bump in fd7a25af11e cause this patch to not apply.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v20260403_4-0001-Online-enabling-and-disabling-of-data-ch.patch (226.1K, ../../[email protected]/2-v20260403_4-0001-Online-enabling-and-disabling-of-data-ch.patch)
download | inline diff:
From b7d2a65ff33ff95423412eac28c4ff40d966aec0 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Fri, 3 Apr 2026 21:26:21 +0200
Subject: [PATCH v20260403_4] Online enabling and disabling of data checksums
This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.
Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.
A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calculated on write. Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.
When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading. Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading. The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all. This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.
A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes. The tests which run concurrent pgbdench during online
processing are gated behind the PG_TEST_EXTRA flag due to being
very expensive to run. Two levels of PG_TEST_EXTRA flags exist
to turn on a subset of the expensive tests, or the full suite of
multiple runs.
This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin. During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.
Author: Daniel Gustafsson <[email protected]>
Author: Magnus Hagander <[email protected]>
Co-authored-by: Tomas Vondra <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Heikki Linnakangas <[email protected]>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
doc/src/sgml/config.sgml | 1 +
doc/src/sgml/func/func-admin.sgml | 78 +
doc/src/sgml/glossary.sgml | 24 +
doc/src/sgml/images/Makefile | 1 +
doc/src/sgml/images/datachecksums.gv | 14 +
doc/src/sgml/images/datachecksums.svg | 81 +
doc/src/sgml/monitoring.sgml | 228 ++-
doc/src/sgml/ref/pg_checksums.sgml | 6 +
doc/src/sgml/regress.sgml | 14 +
doc/src/sgml/wal.sgml | 126 +-
src/backend/access/rmgrdesc/xlogdesc.c | 58 +-
src/backend/access/transam/xlog.c | 502 ++++-
src/backend/backup/basebackup.c | 31 +-
src/backend/bootstrap/bootstrap.c | 1 +
src/backend/catalog/system_views.sql | 19 +
src/backend/commands/dbcommands.c | 7 +
src/backend/postmaster/Makefile | 1 +
src/backend/postmaster/auxprocess.c | 19 +
src/backend/postmaster/bgworker.c | 10 +-
src/backend/postmaster/datachecksum_state.c | 1612 +++++++++++++++++
src/backend/postmaster/meson.build | 1 +
src/backend/postmaster/postmaster.c | 5 +
src/backend/replication/logical/decode.c | 16 +
src/backend/storage/buffer/bufmgr.c | 7 +
src/backend/storage/ipc/ipci.c | 3 +
src/backend/storage/ipc/procsignal.c | 8 +
src/backend/storage/page/README | 4 +-
src/backend/storage/page/bufpage.c | 23 +-
src/backend/utils/activity/pgstat_backend.c | 2 +
src/backend/utils/activity/pgstat_io.c | 2 +
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/pgstatfuncs.c | 8 +-
src/backend/utils/init/miscinit.c | 3 +-
src/backend/utils/init/postinit.c | 20 +-
src/backend/utils/misc/guc_parameters.dat | 5 +-
src/backend/utils/misc/guc_tables.c | 9 +-
src/backend/utils/misc/postgresql.conf.sample | 10 +-
src/bin/pg_checksums/pg_checksums.c | 4 +-
src/bin/pg_controldata/pg_controldata.c | 2 +
src/bin/pg_upgrade/controldata.c | 9 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
src/include/access/rmgrlist.h | 1 +
src/include/access/xlog.h | 17 +-
src/include/access/xlog_internal.h | 8 +
src/include/catalog/catversion.h | 2 +-
src/include/catalog/pg_control.h | 8 +-
src/include/catalog/pg_proc.dat | 14 +
src/include/commands/progress.h | 16 +
src/include/miscadmin.h | 6 +
src/include/postmaster/datachecksum_state.h | 58 +
src/include/postmaster/proctypelist.h | 2 +
src/include/replication/decode.h | 1 +
src/include/storage/bufpage.h | 2 +-
src/include/storage/checksum.h | 16 +
src/include/storage/lwlocklist.h | 1 +
src/include/storage/procsignal.h | 4 +
src/include/utils/backend_progress.h | 1 +
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_checksums/.gitignore | 2 +
src/test/modules/test_checksums/Makefile | 40 +
src/test/modules/test_checksums/README | 30 +
src/test/modules/test_checksums/meson.build | 38 +
.../modules/test_checksums/t/001_basic.pl | 63 +
.../modules/test_checksums/t/002_restarts.pl | 110 ++
.../test_checksums/t/003_standby_restarts.pl | 114 ++
.../modules/test_checksums/t/004_offline.pl | 82 +
.../modules/test_checksums/t/005_injection.pl | 74 +
.../test_checksums/t/006_pgbench_single.pl | 275 +++
.../test_checksums/t/007_pgbench_standby.pl | 400 ++++
src/test/modules/test_checksums/t/008_pitr.pl | 189 ++
src/test/modules/test_checksums/t/009_fpi.pl | 64 +
.../test_checksums/t/DataChecksums/Utils.pm | 262 +++
.../test_checksums/test_checksums--1.0.sql | 24 +
.../modules/test_checksums/test_checksums.c | 184 ++
.../test_checksums/test_checksums.control | 4 +
src/test/perl/PostgreSQL/Test/Cluster.pm | 36 +
src/test/regress/expected/rules.out | 35 +
src/test/regress/expected/stats.out | 18 +-
src/tools/pgindent/typedefs.list | 7 +
80 files changed, 5132 insertions(+), 58 deletions(-)
create mode 100644 doc/src/sgml/images/datachecksums.gv
create mode 100644 doc/src/sgml/images/datachecksums.svg
create mode 100644 src/backend/postmaster/datachecksum_state.c
create mode 100644 src/include/postmaster/datachecksum_state.h
create mode 100644 src/test/modules/test_checksums/.gitignore
create mode 100644 src/test/modules/test_checksums/Makefile
create mode 100644 src/test/modules/test_checksums/README
create mode 100644 src/test/modules/test_checksums/meson.build
create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
create mode 100644 src/test/modules/test_checksums/t/008_pitr.pl
create mode 100644 src/test/modules/test_checksums/t/009_fpi.pl
create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
create mode 100644 src/test/modules/test_checksums/test_checksums.c
create mode 100644 src/test/modules/test_checksums/test_checksums.control
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 422ba304982..d3fea738ca3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -7187,6 +7187,7 @@ local0.* /var/log/postgresql
<member><literal>bgworker</literal></member>
<member><literal>bgwriter</literal></member>
<member><literal>checkpointer</literal></member>
+ <member><literal>checksums</literal></member>
<member><literal>ioworker</literal></member>
<member><literal>postmaster</literal></member>
<member><literal>slotsyncworker</literal></member>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 210b1118bdf..24ecb46542e 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3123,4 +3123,82 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
</sect2>
+ <sect2 id="functions-admin-checksum">
+ <title>Data Checksum Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-checksums-table" /> can
+ be used to enable or disable data checksums in a running cluster.
+ </para>
+ <para>
+ Changing data checksums can be done in a cluster with concurrent activity
+ without blocking queries, but overall system performance will be affected.
+ See <xref linkend="checksums" /> for further details on how changing the
+ data checksums state can affect a system and possible mitigations for how
+ to reduce the impact.
+ </para>
+
+ <table id="functions-checksums-table">
+ <title>Data Checksum Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <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> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Initiates the process of enabling data checksums for the cluster. This
+ will set the data checksums state to <literal>inprogress-on</literal>
+ as well as start a background worker that will process all pages in all
+ databases and enable data checksums on them. When all pages have
+ been processed, the cluster will automatically set data checksums state
+ to <literal>on</literal>. This operation is WAL logged and replicated
+ to all standby nodes.
+ </para>
+ <para>
+ If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
+ specified, the process is throttled using the same principles as
+ <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_disable_data_checksums</primary>
+ </indexterm>
+ <function>pg_disable_data_checksums</function> ()
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Disables data checksum calculation and validation for the cluster. This
+ will set the data checksum state to <literal>inprogress-off</literal>
+ while data checksums are being disabled. When all active backends have
+ stopped validating data checksums, the data checksum state will be
+ set to <literal>off</literal>.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 113d7640626..b881ae71198 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
(but not the autovacuum workers),
the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+ the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
the <glossterm linkend="glossary-logger">logger</glossterm>,
the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
<glosssee otherterm="glossary-data-directory" />
</glossentry>
+ <glossentry id="glossary-data-checksums-worker">
+ <glossterm>Data Checksums Worker</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which enables data checksums in a specific database.
+ </para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry id="glossary-data-checksums-worker-launcher">
+ <glossterm>Data Checksums Worker Launcher</glossterm>
+ <glossdef>
+ <para>
+ A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+ which starts <glossterm linkend="glossary-data-checksums-worker"> data
+ checksum worker</glossterm> processes for enabling data checksums in each
+ database, or disables data checksums cluster-wide.
+ </para>
+ </glossdef>
+ </glossentry>
+
<glossentry id="glossary-database">
<glossterm>Database</glossterm>
<glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..7b8ac0fbb32 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
# see README in this directory about image handling
ALL_IMAGES = \
+ datachecksums.svg \
genetic-algorithm.svg \
gin.svg \
pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+ A -> B [label="SELECT pg_enable_data_checksums()"];
+ B -> C;
+ D -> A;
+ C -> D [label="SELECT pg_disable_data_checksums()"];
+ E -> A [label=" --no-data-checksums"];
+ E -> C [label=" --data-checksums"];
+
+ A [label="off"];
+ B [label="inprogress-on"];
+ C [label="on"];
+ D [label="inprogress-off"];
+ E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress-on</text>
+</g>
+<!-- A->B -->
+<g id="edge1" class="edge">
+<title>A->B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B->C -->
+<g id="edge2" class="edge">
+<title>B->C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress-off</text>
+</g>
+<!-- C->D -->
+<g id="edge4" class="edge">
+<title>C->D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D->A -->
+<g id="edge3" class="edge">
+<title>D->A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E->A -->
+<g id="edge5" class="edge">
+<title>E->A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> --no-data-checksums</text>
+</g>
+<!-- E->C -->
+<g id="edge6" class="edge">
+<title>E->C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> --data-checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..312374da5e0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3885,9 +3885,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of data page checksum failures detected in this
- database (or on a shared object), or NULL if data checksums are
- disabled.
- </para></entry>
+ database (or on a shared object). Detected failures are not reset if
+ the <xref linkend="guc-data-checksums"/> setting changes. Clusters
+ which are initialized without data checksums will show this as
+ <literal>0</literal>. In <productname>PostgreSQL</productname> version
+ 18 and earlier, this was set to <literal>NULL</literal> for clusters
+ with data checksums disabled.
+ </para>
+ </entry>
</row>
<row>
@@ -3896,8 +3901,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Time at which the last data page checksum failure was detected in
- this database (or on a shared object), or NULL if data checksums are
- disabled.
+ this database (or on a shared object). Last failure is reported
+ regardless of the <xref linkend="guc-data-checksums"/> setting.
</para></entry>
</row>
@@ -7634,6 +7639,219 @@ FROM pg_stat_get_backend_idset() AS backendid;
</sect2>
+ <sect2 id="data-checksum-progress-reporting">
+ <title>Data Checksum Progress Reporting</title>
+
+ <indexterm>
+ <primary>pg_stat_progress_data_checksums</primary>
+ </indexterm>
+
+ <para>
+ When data checksums are being enabled on a running cluster, the
+ <structname>pg_stat_progress_data_checksums</structname> view will contain
+ a row for the launcher process, and one row for each worker process which
+ is currently calculating and writing checksums for the data pages in a database.
+ The launcher provides overview of the overall progress (how many databases
+ have been processed, how many remain), while the workers track progress for
+ currently processed databases.
+ </para>
+
+ <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+ <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>pid</structfield> <type>integer</type>
+ </para>
+ <para>
+ Process ID of the data checksum process, launcher or worker.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datid</structfield> <type>oid</type>
+ </para>
+ <para>
+ OID of this database, or <literal>0</literal> for the launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>datname</structfield> <type>name</type>
+ </para>
+ <para>
+ Name of this database, or <literal>NULL</literal> for the
+ launcher process.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>phase</structfield> <type>text</type>
+ </para>
+ <para>
+ Current processing phase, see <xref linkend="datachecksum-phases"/>
+ for description of the phases.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of databases which will be processed. Only the
+ launcher process has this value set, the worker processes have this
+ set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>databases_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of databases which have been processed. Only the launcher
+ process has this value set, the worker processes have this set to
+ <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The total number of relations which will be processed, or
+ <literal>NULL</literal> if the worker process hasn't
+ calculated the number of relations yet. The launcher process has
+ this set to <literal>NULL</literal> since it isn't responsible for
+ processing relations, only launching worker processes.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>relations_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of relations which have been processed. The launcher
+ process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_total</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which will be processed,
+ or <literal>NULL</literal> if the worker process hasn't
+ calculated the number of blocks yet. The launcher process has
+ this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>blocks_done</structfield> <type>integer</type>
+ </para>
+ <para>
+ The number of blocks in the current relation which have been processed.
+ The launcher process has this set to <literal>NULL</literal>.
+ </para>
+ </entry>
+ </row>
+
+ </tbody>
+ </tgroup>
+ </table>
+
+ <table id="datachecksum-phases">
+ <title>Data Checksum Phases</title>
+ <tgroup cols="2">
+ <colspec colname="col1" colwidth="1*"/>
+ <colspec colname="col2" colwidth="2*"/>
+ <thead>
+ <row>
+ <entry>Phase</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><literal>enabling</literal></entry>
+ <entry>
+ The command is currently enabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>disabling</literal></entry>
+ <entry>
+ The command is currently disabling data checksums on the cluster.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>done</literal></entry>
+ <entry>
+ The command is done and the data checksum state in the cluster has
+ changed.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on barrier</literal></entry>
+ <entry>
+ The command is currently waiting for the current active backends to
+ acknowledge the change in data checksum state.
+ </entry>
+ </row>
+ <row>
+ <entry><literal>waiting on temporary tables</literal></entry>
+ <entry>
+ The command is currently waiting for all temporary tables which existed
+ at the time the command was started to be removed.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
</sect1>
<sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index b64393c813f..45890324075 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
exit status is nonzero if the operation failed.
</para>
+ <para>
+ When enabling checksums, if checksums were in the process of being enabled
+ when the cluster was shut down, <application>pg_checksums</application>
+ will still process all relations regardless of the online processing.
+ </para>
+
<para>
When verifying checksums, every file in the cluster is scanned. When
enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index 873387ec168..c74941bfbf2 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,20 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</programlisting>
The following values are currently supported:
<variablelist>
+ <varlistentry>
+ <term><literal>checksum</literal>, <literal>checksum_extended</literal></term>
+ <listitem>
+ <para>
+ Runs additional tests for enabling data checksums which inject faults
+ to cause re-tries in the processing, as well as tests that run pgbench
+ concurrently and randomly restarts the cluster. Some of these test
+ suites require injection points enabled in the installation.
+ <literal>checksum_extended</literal> is an extended version with
+ longer runtime, injected random delays and larger datasets.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>kerberos</literal></term>
<listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..165af8a0cf2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
<para>
Checksums can be disabled when the cluster is initialized using <link
linkend="app-initdb-data-checksums"><application>initdb</application></link>.
- They can also be enabled or disabled at a later time as an offline
- operation. Data checksums are enabled or disabled at the full cluster
- level, and cannot be specified individually for databases or tables.
+ They can also be enabled or disabled at a later time either as an offline
+ operation or online in a running cluster allowing concurrent access. Data
+ checksums are enabled or disabled at the full cluster level, and cannot be
+ specified individually for databases, tables or replicated cluster members.
</para>
<para>
@@ -265,7 +266,7 @@
</para>
<sect2 id="checksums-offline-enable-disable">
- <title>Off-line Enabling of Checksums</title>
+ <title>Offline Enabling of Checksums</title>
<para>
The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,123 @@
</para>
</sect2>
+
+ <sect2 id="checksums-online-enable-disable">
+ <title>Online Enabling of Checksums</title>
+
+ <para>
+ Checksums can be enabled or disabled online, by calling the appropriate
+ <link linkend="functions-admin-checksum">functions</link>.
+ </para>
+
+ <para>
+ Both enabling and disabling data checksums happens in two phases, separated
+ by a checkpoint to ensure durability. The different states, and their
+ transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+ and discussed in further detail in this section.
+ </para>
+
+ <para>
+ <figure id="data-checksums-states-figure">
+ <title>data checksums states</title>
+ <mediaobject>
+ <imageobject>
+ <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+ </imageobject>
+ </mediaobject>
+ </figure>
+ </para>
+
+ <para>
+ Enabling checksums will set the cluster checksum state to
+ <literal>inprogress-on</literal>. During this time, checksums will be
+ written but not verified. In addition to this, a background worker process
+ is started that enables checksums on all existing data in the cluster. Once
+ this worker has completed processing all databases in the cluster, the
+ checksum state will automatically switch to <literal>on</literal>. The
+ processing will consume two background worker processes, make sure that
+ <varname>max_worker_processes</varname> allows for at least two more
+ additional processes.
+ </para>
+
+ <para>
+ The process will initially wait for all open transactions to finish before
+ it starts, so that it can be certain that there are no tables that have been
+ created inside a transaction that has not committed yet and thus would not
+ be visible to the process enabling checksums. It will also, for each database,
+ wait for all pre-existing temporary tables to get removed before it finishes.
+ If long-lived temporary tables are used in an application it may be necessary
+ to terminate these application connections to allow the process to complete.
+ </para>
+
+ <para>
+ If the cluster is stopped while in <literal>inprogress-on</literal> state,
+ for any reason, or processing was interrupted, then the checksum enable
+ process must be restarted manually. To do this, re-execute the function
+ <function>pg_enable_data_checksums()</function> once the cluster has been
+ restarted. The process will start over, there is no support for resuming
+ work from where it was interrupted. If the cluster is stopped while in
+ <literal>inprogress-off</literal>, then the checksum state will be set to
+ <literal>off</literal> when the cluster is restarted.
+ </para>
+
+ <para>
+ Disabling data checksums will set the data checksum state to
+ <literal>inprogress-off</literal>. During this time, checksums will be
+ written but not verified. After all processes acknowledge the change,
+ the state will automatically be set to <literal>off</literal>.
+ </para>
+
+ <para>
+ Disabling data checksums while data checksums are actively being enabled
+ will abort the current processing.
+ </para>
+
+ <sect3 id="checksums-online-system-impact">
+ <title>Impact on system of online operations</title>
+ <para>
+ Enabling data checksums can cause significant I/O to the system, as all of the
+ database pages will need to be rewritten, and will be written both to the
+ data files and the WAL. The impact may be limited by throttling using the
+ <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+ parameters of the <function>pg_enable_data_checksums()</function> function.
+ </para>
+
+ <para>
+ <itemizedlist>
+ <listitem><para>
+ I/O: all pages need to have data checksums calculated and written which
+ will generate a lot of dirty pages that will need to be flushed to disk,
+ as well as WAL logged.
+ </para></listitem>
+ <listitem><para>
+ Replication: When the standby receives the data checksum state change
+ in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+ restartpoint</glossterm> in order to flush the current state into the
+ <filename>pg_control</filename> file. The restartpoint will flush the
+ current state to disk and will block redo until finished. This in turn
+ will induce replication lag, which on synchronous standbys also blocks
+ the primary. Reducing <xref linkend="guc-max-wal-size"/> before the
+ process is started can help with reducing the time it takes for the
+ restartpoint to finish.
+ </para></listitem>
+ <listitem><para>
+ Shutdown/Restart: If the server is shut down or restarted when data
+ checksums are being enabled, the process will not resume and all pages
+ need to be recalculated and rewritten. Enabling data checksums should
+ be done when there is no need for regular maintenance or during a
+ service window.
+ </para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>
+ No I/O is incurred when disabling data checksums, but checkpoints are
+ still required.
+ </para>
+ </sect3>
+
+ </sect2>
</sect1>
<sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 44194d3ea17..2468a7d2578 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "catalog/pg_control.h"
+#include "storage/checksum.h"
#include "utils/guc.h"
#include "utils/timestamp.h"
@@ -54,6 +55,40 @@ get_wal_level_string(int wal_level)
return wal_level_str;
}
+const char *
+get_checksum_state_string(uint32 state)
+{
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_VERSION:
+ return "on";
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ return "inprogress-off";
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ return "inprogress-on";
+ case PG_DATA_CHECKSUM_OFF:
+ return "off";
+ }
+
+ Assert(false);
+ return "?";
+}
+
+void
+xlog2_desc(StringInfo buf, XLogReaderState *record)
+{
+ char *rec = XLogRecGetData(record);
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state xlrec;
+
+ memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+ appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state));
+ }
+}
+
void
xlog_desc(StringInfo buf, XLogReaderState *record)
{
@@ -69,7 +104,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
"tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
"oldest xid %u in DB %u; oldest multi %u in DB %u; "
"oldest/newest commit timestamp xid: %u/%u; "
- "oldest running xid %u; %s",
+ "oldest running xid %u; "
+ "checksums %s; %s",
LSN_FORMAT_ARGS(checkpoint->redo),
checkpoint->ThisTimeLineID,
checkpoint->PrevTimeLineID,
@@ -88,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
checkpoint->oldestCommitTsXid,
checkpoint->newestCommitTsXid,
checkpoint->oldestActiveXid,
+ get_checksum_state_string(checkpoint->dataChecksumState),
(info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
}
else if (info == XLOG_NEXTOID)
@@ -166,7 +203,9 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
xl_checkpoint_redo xlrec;
memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
- appendStringInfo(buf, "wal_level %s", get_wal_level_string(xlrec.wal_level));
+ appendStringInfo(buf, "wal_level %s; checksums %s",
+ get_wal_level_string(xlrec.wal_level),
+ get_checksum_state_string(xlrec.data_checksum_version));
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -241,6 +280,21 @@ xlog_identify(uint8 info)
return id;
}
+const char *
+xlog2_identify(uint8 info)
+{
+ const char *id = NULL;
+
+ switch (info & ~XLR_INFO_MASK)
+ {
+ case XLOG2_CHECKSUMS:
+ id = "CHECKSUMS";
+ break;
+ }
+
+ return id;
+}
+
/*
* Returns a string giving information about all the blocks in an
* XLogRecord.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2c1c6f88b74..9e8999bbb61 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,6 +75,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/startup.h"
#include "postmaster/walsummarizer.h"
#include "postmaster/walwriter.h"
@@ -92,6 +93,7 @@
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
+#include "storage/procsignal.h"
#include "storage/reinit.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -553,6 +555,9 @@ typedef struct XLogCtlData
*/
XLogRecPtr lastFpwDisableRecPtr;
+ /* last data_checksum_version we've seen */
+ uint32 data_checksum_version;
+
slock_t info_lck; /* locks shared variables shown above */
} XLogCtlData;
@@ -650,6 +655,21 @@ static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;
+/*
+ * Local state for Controlfile data_checksum_version. After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing. The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state. Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumStateType LocalDataChecksumState = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
+ * See SetLocalDataChecksumState().
+ */
+int data_checksums = 0;
+
/* For WALInsertLockAcquire/Release functions */
static int MyLockNo = 0;
static bool holdingAllLocks = false;
@@ -717,6 +737,8 @@ static void WALInsertLockAcquireExclusive(void);
static void WALInsertLockRelease(void);
static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
+static void XLogChecksums(uint32 new_type);
+
/*
* Insert an XLOG record represented by an already-constructed chain of data
* chunks. This is a low-level routine; to construct the WAL record header
@@ -4254,6 +4276,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+
+ /*
+ * Set the data_checksum_version value into XLogCtl, which is where all
+ * processes get the current value from.
+ */
+ XLogCtl->data_checksum_version = data_checksum_version;
}
static void
@@ -4588,10 +4616,6 @@ ReadControlFile(void)
(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
CalculateCheckpointSegments();
-
- /* Make the initdb settings visible as GUC variables, too */
- SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
- PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}
/*
@@ -4625,13 +4649,323 @@ GetMockAuthenticationNonce(void)
}
/*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ * Returns whether data checksums must be written or not
+ *
+ * Returns true if data checksums are enabled, or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksum_state.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON ||
+ LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
+}
+
+bool
+DataChecksumsInProgressOn(void)
+{
+ return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+}
+
+/*
+ * DataChecksumsNeedVerify
+ * Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksum_state.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this. Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
*/
bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
{
+ return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ * Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOnInProgress(void)
+{
+ uint64 barrier;
+
Assert(ControlFile != NULL);
- return (ControlFile->data_checksum_version > 0);
+
+ /*
+ * The state transition is performed in a critical section with
+ * checkpoints held off to provide crash safety.
+ */
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ /*
+ * Await state change in all backends to ensure that all backends are in
+ * "inprogress-on". Once done we know that all backends are writing data
+ * checksums.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOn
+ * Set data checksums state to 'on' cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksum_state.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process. When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /*
+ * The only allowed state transition to "on" is from "inprogress-on" since
+ * that state ensures that all pages will have data checksums written. No
+ * such state transition exists, if it does happen it's likely due to a
+ * programmer error.
+ */
+ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ elog(WARNING,
+ "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+ SetDataChecksumsOff();
+ return;
+ }
+
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ /*
+ * Update the controlfile before waiting since if we have an immediate
+ * shutdown while waiting we want to come back up with checksums enabled.
+ */
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Await state transition to "on" in all backends. When done we know that
+ * data data checksums are both written and verified in all backends.
+ */
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ * Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
+{
+ uint64 barrier;
+
+ Assert(ControlFile != NULL);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+
+ /* If data checksums are already disabled there is nothing to do */
+ if (XLogCtl->data_checksum_version == 0)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+ return;
+ }
+
+ /*
+ * If data checksums are currently enabled we first transition to the
+ * "inprogress-off" state during which backends continue to write
+ * checksums without verifying them. When all backends are in
+ * "inprogress-off" the next transition to "off" can be performed, after
+ * which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ {
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ START_CRIT_SECTION();
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ /*
+ * Update local state in all backends to ensure that any backend in
+ * "on" state is changed to "inprogress-off".
+ */
+ WaitForProcSignalBarrier(barrier);
+
+ /*
+ * At this point we know that no backends are verifying data checksums
+ * during reading. Next, we can safely move to state "off" to also
+ * stop writing checksums.
+ */
+ }
+ else
+ {
+ /*
+ * Ending up here implies that the checksums state is "inprogress-on"
+ * or "inprogress-off" and we can transition directly to "off" from
+ * there.
+ */
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
+ START_CRIT_SECTION();
+ /* Ensure that we don't incur a checkpoint during disabling checksums */
+ MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * InitLocalDataChecksumState
+ *
+ * Set up backend local caches of controldata variables which may change at
+ * any point during runtime and thus require special cased locking. So far
+ * this only applies to data_checksum_version, but it's intended to be general
+ * purpose enough to handle future cases.
+ */
+void
+InitLocalDataChecksumState(void)
+{
+ SpinLockAcquire(&XLogCtl->info_lck);
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+}
+
+void
+SetLocalDataChecksumState(uint32 data_checksum_version)
+{
+ LocalDataChecksumState = data_checksum_version;
+
+ data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+ return get_checksum_state_string(LocalDataChecksumState);
}
/*
@@ -4925,6 +5259,7 @@ LocalProcessControlFile(bool reset)
Assert(reset || ControlFile == NULL);
ControlFile = palloc_object(ControlFileData);
ReadControlFile();
+ SetLocalDataChecksumState(ControlFile->data_checksum_version);
}
/*
@@ -5094,6 +5429,11 @@ XLOGShmemInit(void)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ /* Use the checksum info from control file */
+ XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5168,6 +5508,7 @@ BootStrapXLOG(uint32 data_checksum_version)
checkPoint.newestCommitTsXid = InvalidTransactionId;
checkPoint.time = (pg_time_t) time(NULL);
checkPoint.oldestActiveXid = InvalidTransactionId;
+ checkPoint.dataChecksumState = data_checksum_version;
TransamVariables->nextXid = checkPoint.nextXid;
TransamVariables->nextOid = checkPoint.nextOid;
@@ -6244,6 +6585,47 @@ StartupXLOG(void)
pfree(endOfRecoveryInfo->recoveryStopReason);
pfree(endOfRecoveryInfo);
+ /*
+ * If we reach this point with checksums in the state inprogress-on, it
+ * means that data checksums were in the process of being enabled when the
+ * cluster shut down. Since processing didn't finish, the operation will
+ * have to be restarted from scratch since there is no capability to
+ * continue where it was when the cluster shut down. Thus, revert the
+ * state back to off, and inform the user with a warning message. Being
+ * able to restart processing is a TODO, but it wouldn't be possible to
+ * restart here since we cannot launch a dynamic background worker
+ * directly from here (it has to be from a regular backend).
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ ereport(WARNING,
+ errmsg("enabling data checksums was interrupted"),
+ errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
+ }
+
+ /*
+ * If data checksums were being disabled when the cluster was shut down,
+ * we know that we have a state where all backends have stopped validating
+ * checksums and we can move to off instead of prompting the user to
+ * perform any action.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ {
+ XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = 0;
+ SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ SpinLockRelease(&XLogCtl->info_lck);
+ }
+
/*
* All done with end-of-recovery actions.
*
@@ -6549,7 +6931,7 @@ GetRedoRecPtr(void)
XLogRecPtr ptr;
/*
- * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+ * The possibly not up-to-date copy in XLogCtl is enough. Even if we
* grabbed a WAL insertion lock to read the authoritative value in
* Insert->RedoRecPtr, someone might update it just after we've released
* the lock.
@@ -7127,6 +7509,12 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ /*
+ * Get the current data_checksum_version value from xlogctl, valid at the
+ * time of the checkpoint.
+ */
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7183,6 +7571,9 @@ CreateCheckPoint(int flags)
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
+ SpinLockAcquire(&XLogCtl->info_lck);
+ redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
/* Include WAL level in record for WAL summarizer's benefit. */
@@ -7243,6 +7634,10 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7787,9 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
+ /* make sure we start with the checksum version as of the checkpoint */
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +7933,12 @@ CreateEndOfRecoveryRecord(void)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->minRecoveryPoint = recptr;
ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+ /* start with the latest checksum version (as of the end of recovery) */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -7876,6 +8280,10 @@ CreateRestartPoint(int flags)
if (flags & CHECKPOINT_IS_SHUTDOWN)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+
+ /* we shall start with the latest checksum version */
+ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+
UpdateControlFile();
}
LWLockRelease(ControlFileLock);
@@ -8314,6 +8722,24 @@ XLogReportParameters(void)
}
}
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+ xl_checksum_state xlrec;
+ XLogRecPtr recptr;
+
+ xlrec.new_checksum_state = new_type;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+ recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
+ XLogFlush(recptr);
+}
+
/*
* Update full_page_writes in shared memory, and write an
* XLOG_FPW_CHANGE record if necessary.
@@ -8440,6 +8866,11 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ SetLocalDataChecksumState(checkPoint.dataChecksumState);
+ SpinLockRelease(&XLogCtl->info_lck);
+
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8499,6 +8930,7 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/*
@@ -8525,6 +8957,8 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ bool new_state = false;
+ int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8563,6 +8997,8 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+ old_state = ControlFile->data_checksum_version;
+ ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -8574,6 +9010,18 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
+ /*
+ * If the data checksum state change we need to emit a barrier.
+ */
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+ if (checkPoint.dataChecksumState != old_state)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
+
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -8735,7 +9183,19 @@ xlog_redo(XLogReaderState *record)
}
else if (info == XLOG_CHECKPOINT_REDO)
{
- /* nothing to do here, just for informational purposes */
+ xl_checkpoint_redo redo_rec;
+ bool new_state = false;
+
+ memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+ new_state = true;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ if (new_state)
+ EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
{
@@ -8788,6 +9248,30 @@ xlog_redo(XLogReaderState *record)
}
}
+void
+xlog2_redo(XLogReaderState *record)
+{
+ uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+ if (info == XLOG2_CHECKSUMS)
+ {
+ xl_checksum_state state;
+
+ memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ XLogCtl->data_checksum_version = state.new_checksum_state;
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ /*
+ * Block on a procsignalbarrier to await all processes having seen the
+ * change to checksum status. Once the barrier has been passed we can
+ * initiate the corresponding processing.
+ */
+ EmitAndWaitDataChecksumsBarrier(state.new_checksum_state);
+ }
+}
+
/*
* Return the extra open flags used for opening a file, depending on the
* value of the GUCs wal_sync_method, fsync and debug_io_direct.
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index ab1fbae8001..9c79dadaacc 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
/*
* If we weren't told not to verify checksums, and if checksums are
* enabled for this cluster, and if this is a relation file, then verify
- * the checksum.
+ * the checksum. We cannot at this point check if checksums are enabled
+ * or disabled as that might change, thus we check at each point where we
+ * could be validating a checksum.
*/
- if (!noverify_checksums && DataChecksumsEnabled() &&
- RelFileNumberIsValid(relfilenumber))
+ if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
verify_checksum = true;
/*
@@ -1749,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* If the amount of data we were able to read was not a multiple of
* BLCKSZ, we cannot verify checksums, which are block-level.
*/
- if (verify_checksum && (cnt % BLCKSZ != 0))
+ if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
{
ereport(WARNING,
(errmsg("could not verify checksum in file \"%s\", block "
@@ -1844,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
* 'blkno' is the block number of the first page in the bbsink's buffer
* relative to the start of the relation.
*
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages. If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
*/
static off_t
read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1872,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
int reread_cnt;
uint16 expected_checksum;
+ /*
+ * The data checksum state can change at any point, so we need to
+ * re-check before each page.
+ */
+ if (!DataChecksumsNeedVerify())
+ return cnt;
+
page = sink->bbs_buffer + BLCKSZ * i;
/* If the page is OK, go on to the next one. */
@@ -1894,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
* allows us to wait until we can be certain that no write to the
* block is in progress. Since we don't have any such thing right now,
* we just do this and hope for the best.
+ *
+ * The data checksum state may also have changed concurrently so check
+ * again.
*/
+ if (!DataChecksumsNeedVerify())
+ return cnt;
reread_cnt =
basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
BLCKSZ, offset + BLCKSZ * i,
@@ -2009,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
return true;
+ if (!DataChecksumsNeedVerify())
+ return true;
+
/* Perform the actual checksum calculation. */
checksum = pg_checksum_page(page, blkno);
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 38ef683d4c7..c52c0a6023d 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -35,6 +35,7 @@
#include "port/pg_getopt_ctx.h"
#include "postmaster/postmaster.h"
#include "storage/bufpage.h"
+#include "storage/checksum.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/proc.h"
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..eba25aa3e4d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1451,6 +1451,25 @@ CREATE VIEW pg_stat_progress_copy AS
FROM pg_stat_get_progress_info('COPY') AS S
LEFT JOIN pg_database D ON S.datid = D.oid;
+CREATE VIEW pg_stat_progress_data_checksums AS
+ SELECT
+ S.pid AS pid, S.datid, D.datname AS datname,
+ CASE S.param1 WHEN 0 THEN 'enabling'
+ WHEN 1 THEN 'disabling'
+ WHEN 2 THEN 'waiting on temporary tables'
+ WHEN 3 THEN 'waiting on barrier'
+ WHEN 4 THEN 'done'
+ END AS phase,
+ CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+ S.param3 AS databases_done,
+ CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+ CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+ CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+ CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+ FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+ LEFT JOIN pg_database D ON S.datid = D.oid
+ ORDER BY S.datid; -- return the launcher process first
+
CREATE VIEW pg_user_mappings AS
SELECT
U.oid AS umid,
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index 9b18bb4a17e..f0819d15ab7 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
if (pg_strcasecmp(strategy, "wal_log") == 0)
dbstrategy = CREATEDB_WAL_LOG;
else if (pg_strcasecmp(strategy, "file_copy") == 0)
+ {
+ if (DataChecksumsInProgressOn())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
+ strategy));
dbstrategy = CREATEDB_FILE_COPY;
+ }
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..55044b2bc6f 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
bgworker.o \
bgwriter.o \
checkpointer.o \
+ datachecksum_state.o \
fork_process.o \
interrupt.o \
launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index cf24f662d27..8fdc518b3a1 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
#include <unistd.h>
#include <signal.h>
+#include "access/xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/auxprocess.h"
@@ -69,6 +70,24 @@ AuxiliaryProcessMainCommon(void)
ProcSignalInit(NULL, 0);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized - but it can happen only once.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index f2a62489d9c..536aff7ca05 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
#include "pgstat.h"
#include "port/atomics.h"
#include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/postmaster.h"
#include "replication/logicallauncher.h"
#include "replication/logicalworker.h"
@@ -144,7 +145,14 @@ static const struct
.fn_name = "TableSyncWorkerMain",
.fn_addr = TableSyncWorkerMain
},
-
+ {
+ .fn_name = "DataChecksumsWorkerLauncherMain",
+ .fn_addr = DataChecksumsWorkerLauncherMain
+ },
+ {
+ .fn_name = "DataChecksumsWorkerMain",
+ .fn_addr = DataChecksumsWorkerMain
+ }
};
/* Private functions. */
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
new file mode 100644
index 00000000000..76004bcedc6
--- /dev/null
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -0,0 +1,1612 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.c
+ * Background worker for enabling or disabling data checksums online as
+ * well as functionality for manipulating data checksum state
+ *
+ * When enabling data checksums on a cluster at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed. When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * When enabling checksums in an online cluster, data_checksums will be set to
+ * "inprogress-on" which signals that write operations MUST compute and write
+ * the checksum on the data page, but during reading the checksum SHALL NOT be
+ * verified. This ensures that all objects created during when checksums are
+ * being enabled will have checksums set, but reads won't fail due to missing or
+ * invalid checksums. Invalid checksums can be present in case the cluster had
+ * checksums enabled, then disabled them and updated the page while they were
+ * disabled.
+ *
+ * The DataChecksumsWorker will compile a list of all databases at the start,
+ * any databases created concurrently will see the in-progress state and will
+ * be checksummed automatically. All databases from the original list MUST BE
+ * successfully processed in order for data checksums to be enabled, the only
+ * exception are databases which are dropped before having been processed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer need to be verified.
+ * This ensures that backends which have not yet transitioned to the
+ * "inprogress-off" state will still see valid checksums on pages.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling or disabling data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ * - Backends SHALL NOT violate the data_checksums state they have agreed to
+ * by acknowledging the procsignalbarrier: This means that all backends
+ * MUST calculate and write data checksums during all states except off;
+ * MUST validate checksums only in the 'on' state.
+ * - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ * currently connected backends have state "on": This means that all
+ * backends must wait on the procsignalbarrier to be acknowledged by all
+ * before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers. Before
+ * updating the data_checksums state in the control file, all other backends must absorb the
+ * barrier. Barrier absorption will happen during interrupt processing, which
+ * means that connected backends will change state at different times. If
+ * waiting for a barrier is done during startup, for example during replay, it
+ * is important to realize that any locks held by the startup process might
+ * cause deadlocks if backends end up waiting for those locks while startup
+ * is waiting for a procsignalbarrier.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one if Bd and Bi being empty:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * If processing was halted by the cluster shutting down (due to a crash or
+ * intentional restart), the controlfile state "inprogress-on" will be observed
+ * on system startup and all backends will be placed in Bd. The controlfile
+ * state will also be set to "off".
+ *
+ * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
+ * DataChecksumsLauncher. When all backends have acknowledged the barrier then
+ * Bd will be empty and the next phase can begin: calculating and writing data
+ * checksums with DataChecksumsWorkers. When the DataChecksumsWorker processes
+ * have finished writing checksums on all pages, data checksums are enabled
+ * cluster-wide via another procsignalbarrier. There are four sets of backends
+ * where Bd shall be an empty set:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends in Bi and Be will write checksums when modifying a page, but only
+ * backends in Be will verify the checksum during reading. The Bg backend is
+ * blocked waiting for all backends in Bi to process interrupts and move to
+ * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
+ * observe the global state being "on" and will thus automatically belong to
+ * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
+ * are compatible sets while still operating based on their local state as
+ * both write data checksums.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful. The latter will cause errors
+ * in user operations. Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ * * Launching datachecksumsworker for resuming operation from the startup
+ * process: Currently users have to restart processing manually after a
+ * restart since dynamic background worker cannot be started from the
+ * postmaster. Changing the startup process could make restarting the
+ * processing automatic on cluster restart.
+ * * Avoid dirtying the page when checksums already match: Iff the checksum
+ * on the page happens to already match we still dirty the page. It should
+ * be enough to only do the log_newpage_buffer() call in that case.
+ * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ * to enable checksums on a cluster which is in inprogress-on state and
+ * may have checksummed pages (make pg_checksums be able to resume an
+ * online operation). This should only be attempted for wal_level minimal.
+ * * Restartability (not necessarily with page granularity).
+ * * Avoid processing databases which were created during inprogress-on.
+ * Right now all databases are processed regardless to be safe.
+ * * Teach CREATE DATABASE to calculate checksums for databases created
+ * during inprogress-on with a template database which has yet to be
+ * processed.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/postmaster/datachecksum_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+#include "utils/wait_event.h"
+
+/*
+ * Configuration of conditions which must match when absorbing a procsignal
+ * barrier during data checksum enable/disable operations. A single function
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
+ */
+typedef struct ChecksumBarrierCondition
+{
+ /* Current state of data checksums */
+ int from;
+ /* Target state for data checksums */
+ int to;
+} ChecksumBarrierCondition;
+
+static const ChecksumBarrierCondition checksum_barriers[6] =
+{
+ /*
+ * Disabling checksums: If checksums are currently enabled, disabling must
+ * go through the 'inprogress-off' state.
+ */
+ {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * If checksums are in the process of being enabled, but are not yet being
+ * verified, we can abort by going back to 'off' state.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+ /*
+ * Enabling checksums must normally go through the 'inprogress-on' state.
+ */
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being disabled but all backends are still computing
+ * checksums, we can go straight back to 'on'
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+};
+
+/*
+ * Signaling between backends calling pg_enable/disable_data_checksums, the
+ * checksums launcher process, and the checksums worker process.
+ *
+ * This struct is protected by DataChecksumsWorkerLock
+ */
+typedef struct DataChecksumsStateStruct
+{
+ /*
+ * These are set by pg_{enable|disable}_data_checksums, to tell the
+ * launcher what the target state is.
+ */
+ DataChecksumsWorkerOperation launch_operation;
+ int launch_cost_delay;
+ int launch_cost_limit;
+
+ /*
+ * Is a launcher process is currently running? This is set by the main
+ * launcher process, after it has read the above launch_* parameters.
+ */
+ bool launcher_running;
+
+ /*
+ * Is a worker process currently running? This is set by the worker
+ * launcher when it starts waiting for a worker process to finish.
+ */
+ int worker_pid;
+
+ /*
+ * These fields indicate the target state that the launcher is currently
+ * working towards. They can be different from the corresponding launch_*
+ * fields, if a new pg_enable/disable_data_checksums() call was made while
+ * the launcher/worker was already running.
+ *
+ * The below members are set when the launcher starts, and are only
+ * accessed read-only by the single worker. Thus, we can access these
+ * without a lock. If multiple workers, or dynamic cost parameters, are
+ * supported at some point then this would need to be revisited.
+ */
+ DataChecksumsWorkerOperation operation;
+ int cost_delay;
+ int cost_limit;
+
+ /*
+ * Signaling between the launcher and the worker process.
+ *
+ * As there is only a single worker, and the launcher won't read these
+ * until the worker exits, they can be accessed without the need for a
+ * lock. If multiple workers are supported then this will have to be
+ * revisited.
+ */
+
+ /* result, set by worker before exiting */
+ DataChecksumsWorkerResult success;
+
+ /*
+ * tells the worker process whether it should also process the shared
+ * catalogs
+ */
+ bool process_shared_catalogs;
+} DataChecksumsStateStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsStateStruct *DataChecksumState;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+ Oid dboid;
+ char *dbname;
+} DataChecksumsWorkerDatabase;
+
+/* Flag set by the interrupt handler */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
+ * If we have, we need to clear it before exiting!
+ */
+static volatile sig_atomic_t launcher_running = false;
+
+/* Are we enabling data checksums, or disabling them? */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static bool DatabaseExists(Oid dboid);
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*****************************************************************************
+ * Functionality for manipulating the data checksum state in the cluster
+ */
+
+void
+EmitAndWaitDataChecksumsBarrier(uint32 state)
+{
+ uint64 barrier;
+
+ switch (state)
+ {
+ case PG_DATA_CHECKSUM_INPROGRESS_ON:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_INPROGRESS_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_VERSION:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ case PG_DATA_CHECKSUM_OFF:
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ WaitForProcSignalBarrier(barrier);
+ break;
+
+ default:
+ Assert(false);
+ }
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ * Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function. The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
+{
+ uint32 target_state;
+ int current = data_checksums;
+ bool found = false;
+
+ /*
+ * Translate the barrier condition to the target state, doing it here
+ * instead of in the procsignal code saves the latter from knowing about
+ * checksum states.
+ */
+ switch (barrier)
+ {
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_ON;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ target_state = PG_DATA_CHECKSUM_VERSION;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ break;
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ target_state = PG_DATA_CHECKSUM_OFF;
+ break;
+ default:
+ elog(ERROR, "incorrect barrier \"%i\" received", barrier);
+ }
+
+ /*
+ * If the target state matches the current state then the barrier has been
+ * repeated.
+ */
+ if (current == target_state)
+ return true;
+
+ /*
+ * If the cluster is in recovery we skip the validation of current state
+ * since the replay is trusted.
+ */
+ if (RecoveryInProgress())
+ {
+ SetLocalDataChecksumState(target_state);
+ return true;
+ }
+
+ /*
+ * Find the barrier condition definition for the target state. Not finding
+ * a condition would be a grave programmer error as the states are a
+ * discrete set.
+ */
+ for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
+ {
+ if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+ found = true;
+ }
+
+ /*
+ * If the relevant state criteria aren't satisfied, throw an error which
+ * will be caught by the procsignal machinery for a later retry.
+ */
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("incorrect data checksum state %i for target state %i",
+ current, target_state));
+
+ SetLocalDataChecksumState(target_state);
+ return true;
+}
+
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+ PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable. Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+ int cost_delay = PG_GETARG_INT32(0);
+ int cost_limit = PG_GETARG_INT32(1);
+
+ if (!superuser())
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to change data checksum state"));
+
+ if (cost_delay < 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost delay cannot be a negative value"));
+
+ if (cost_limit <= 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cost limit must be greater than zero"));
+
+ StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+ PG_RETURN_VOID();
+}
+
+
+/*****************************************************************************
+ * Functionality for running the datachecksumsworker and associated launcher
+ */
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ * Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ bool launcher_running;
+ DataChecksumsWorkerOperation launcher_running_op;
+
+#ifdef USE_ASSERT_CHECKING
+ /* The cost delay settings have no effect when disabling */
+ if (op == DISABLE_DATACHECKSUMS)
+ Assert(cost_delay == 0 && cost_limit == 0);
+#endif
+
+ INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+ /* Store the desired state in shared memory */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ DataChecksumState->launch_operation = op;
+ DataChecksumState->launch_cost_delay = cost_delay;
+ DataChecksumState->launch_cost_limit = cost_limit;
+
+ /* Is the launcher already running? If so, what is it doing? */
+ launcher_running = DataChecksumState->launcher_running;
+ if (launcher_running)
+ launcher_running_op = DataChecksumState->operation;
+
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * Launch a new launcher process, if it's not running already.
+ *
+ * If the launcher is currently busy enabling the checksums, and we want
+ * them disabled (or vice versa), the launcher will notice that at latest
+ * when it's about to exit, and will loop back process the new request. So
+ * if the launcher is already running, we don't need to do anything more
+ * here to abort it.
+ *
+ * If you call pg_enable/disable_data_checksums() twice in a row, before
+ * the launcher has had a chance to start up, we still end up launching it
+ * twice. That's OK, the second invocation will see that a launcher is
+ * already running and exit quickly.
+ *
+ * TODO: We could optimize here and skip launching the launcher, if we are
+ * already in the desired state, i.e. if the checksums are already enabled
+ * and you call pg_enable_data_checksums().
+ */
+ if (!launcher_running)
+ {
+ /*
+ * Prepare the BackgroundWorker and launch it.
+ */
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = (Datum) 0;
+
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("failed to start background worker to process data checksums"));
+ }
+ else
+ {
+ if (launcher_running_op == op)
+ ereport(ERROR,
+ errmsg("data checksum processing already running"));
+ }
+}
+
+/*
+ * ProcessSingleRelationFork
+ * Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+ BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+ char activity[NAMEDATALEN * 2 + 128];
+ char *relns;
+
+ relns = get_namespace_name(RelationGetNamespace(reln));
+
+ /* Report the current relation to pgstat_activity */
+ snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)",
+ (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+ pgstat_report_activity(STATE_RUNNING, activity);
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+ if (relns)
+ pfree(relns);
+
+ /*
+ * We are looping over the blocks which existed at the time of process
+ * start, which is safe since new blocks are created with checksums set
+ * already due to the state being "inprogress-on".
+ */
+ for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+ {
+ Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+ /* Need to get an exclusive lock to mark the buffer as dirty */
+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+ /*
+ * Mark the buffer as dirty and force a full page write. We have to
+ * re-write the page to WAL even if the checksum hasn't changed,
+ * because if there is a replica it might have a slightly different
+ * version of the page with an invalid checksum, caused by unlogged
+ * changes (e.g. hintbits) on the primary happening while checksums
+ * were off. This can happen if there was a valid checksum on the page
+ * at one point in the past, so only when checksums are first on, then
+ * off, and then turned on again. TODO: investigate if this could be
+ * avoided if the checksum is calculated to be correct and wal_level
+ * is set to "minimal",
+ */
+ START_CRIT_SECTION();
+ MarkBufferDirty(buf);
+ log_newpage_buffer(buf, false);
+ END_CRIT_SECTION();
+
+ UnlockReleaseBuffer(buf);
+
+ /*
+ * This is the only place where we check if we are asked to abort, the
+ * abortion will bubble up from here.
+ */
+ Assert(operation == ENABLE_DATACHECKSUMS);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ if (abort_requested)
+ return false;
+
+ /* update the block counter */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ (blknum + 1));
+
+ /*
+ * Processing is re-using the vacuum cost delay for process
+ * throttling, hence why we call vacuum APIs here.
+ */
+ vacuum_delay_point(false);
+ }
+
+ return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ * Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+ Relation rel;
+ bool aborted = false;
+
+ StartTransactionCommand();
+
+ rel = try_relation_open(relationId, AccessShareLock);
+ if (rel == NULL)
+ {
+ /*
+ * Relation no longer exists. We don't consider this an error since
+ * there are no pages in it that need data checksums, and thus return
+ * true. The worker operates off a list of relations generated at the
+ * start of processing, so relations being dropped in the meantime is
+ * to be expected.
+ */
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return true;
+ }
+ RelationGetSmgr(rel);
+
+ for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+ {
+ if (smgrexists(rel->rd_smgr, fnum))
+ {
+ if (!ProcessSingleRelationFork(rel, fnum, strategy))
+ {
+ aborted = true;
+ break;
+ }
+ }
+ }
+ relation_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+ pgstat_report_activity(STATE_IDLE, NULL);
+
+ return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ * Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish. We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+ BackgroundWorker bgw;
+ BackgroundWorkerHandle *bgw_handle;
+ BgwHandleStatus status;
+ pid_t pid;
+ char activity[NAMEDATALEN + 64];
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+
+ memset(&bgw, 0, sizeof(bgw));
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+ snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ bgw.bgw_notify_pid = MyProcPid;
+ bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+ /*
+ * If there are no worker slots available, there is little we can do. If
+ * we retry in a bit it's still unlikely that the user has managed to
+ * reconfigure in the meantime and we'd be run through retries fast.
+ */
+ if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("The \"%s\" setting might be too low.", "max_worker_processes"));
+ return DATACHECKSUMSWORKER_FAILED;
+ }
+
+ status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+ if (status == BGWH_STOPPED)
+ {
+ ereport(WARNING,
+ errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+ db->dbname),
+ errhint("More details on the error might be found in the server log."));
+
+ /*
+ * Heuristic to see if the database was dropped, and if it was we can
+ * treat it as not an error, else treat as fatal and error out. TODO:
+ * this could probably be improved with a tighter check.
+ */
+ if (DatabaseExists(db->dboid))
+ return DATACHECKSUMSWORKER_FAILED;
+ else
+ return DATACHECKSUMSWORKER_DROPDB;
+ }
+
+ /*
+ * If the postmaster crashed we cannot end up with a processed database so
+ * we have no alternative other than exiting. When enabling checksums we
+ * won't at this time have changed the data checksums state in pg_control
+ * to enabled so when the cluster comes back up processing will have to be
+ * restarted.
+ */
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("cannot enable data checksums without the postmaster process"),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ Assert(status == BGWH_STARTED);
+ ereport(LOG,
+ errmsg("initiating data checksum processing in database \"%s\"",
+ db->dbname));
+
+ /* Save the pid of the worker so we can signal it later */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_pid = pid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ snprintf(activity, sizeof(activity) - 1,
+ "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ status = WaitForBackgroundWorkerShutdown(bgw_handle);
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksum processing in \"%s\"",
+ db->dbname),
+ errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+ if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
+ ereport(LOG,
+ errmsg("data checksums processing was aborted in database \"%s\"",
+ db->dbname));
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ DataChecksumState->worker_pid = InvalidPid;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ return DataChecksumState->success;
+}
+
+/*
+ * launcher_exit
+ *
+ * Internal routine for cleaning up state when the launcher process exits. We
+ * need to clean up the abort flag to ensure that processing started again if
+ * it was previously aborted (note: started again, *not* restarted from where
+ * it left off).
+ */
+static void
+launcher_exit(int code, Datum arg)
+{
+ abort_requested = false;
+
+ if (launcher_running)
+ {
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->worker_pid != InvalidPid)
+ {
+ ereport(LOG,
+ errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+ kill(DataChecksumState->worker_pid, SIGTERM);
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ }
+
+ /*
+ * If the launcher is exiting before data checksums are enabled then set
+ * the state to off since processing cannot be resumed.
+ */
+ if (DataChecksumsInProgressOn())
+ SetDataChecksumsOff();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+ int save_errno = errno;
+
+ abort_requested = true;
+
+ /*
+ * There is no sleeping in the main loop, the flag will be checked
+ * periodically in ProcessSingleRelationFork. The worker does however
+ * sleep when waiting for concurrent transactions to end so we still need
+ * to set the latch.
+ */
+ SetLatch(MyLatch);
+
+ errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ * Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+ TransactionId waitforxid;
+
+ LWLockAcquire(XidGenLock, LW_SHARED);
+ waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+ LWLockRelease(XidGenLock);
+
+ while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+ {
+ char activity[64];
+ int rc;
+
+ /* Oldest running xid is older than us, so wait */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for current transactions to finish (waiting for %u)",
+ waitforxid);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ rc = WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+ /*
+ * If the postmaster died we won't be able to enable checksums
+ * cluster-wide so abort and hope to continue when restarted.
+ */
+ if (rc & WL_POSTMASTER_DEATH)
+ ereport(FATAL,
+ errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during data checksums processing"),
+ errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+ CHECK_FOR_INTERRUPTS();
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+ if (DataChecksumState->launch_operation != operation)
+ abort_requested = true;
+ LWLockRelease(DataChecksumsWorkerLock);
+ if (abort_requested)
+ break;
+ }
+
+ pgstat_report_activity(STATE_IDLE, NULL);
+ return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+ on_shmem_exit(launcher_exit, 0);
+
+ ereport(DEBUG1,
+ errmsg("background worker \"datachecksums launcher\" started"));
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGINT, launcher_cancel_handler);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+ pqsignal(SIGUSR2, SIG_IGN);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+ init_ps_display(NULL);
+
+ INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+ if (DataChecksumState->launcher_running)
+ {
+ ereport(LOG,
+ errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+ /* Launcher was already running, let it finish */
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+
+ launcher_running = true;
+
+ /* Initialize a connection to shared catalogs only */
+ BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
+
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->launcher_running = true;
+ DataChecksumState->operation = operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ /*
+ * The target state can change while we are busy enabling/disabling
+ * checksums, if the user calls pg_disable/enable_data_checksums() before
+ * we are finished with the previous request. In that case, we will loop
+ * back here, to process the new request.
+ */
+again:
+
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ if (operation == ENABLE_DATACHECKSUMS)
+ {
+ /*
+ * If we are asked to enable checksums in a cluster which already has
+ * checksums enabled, exit immediately as there is nothing more to do.
+ */
+ if (DataChecksumsNeedVerify())
+ goto done;
+
+ ereport(LOG,
+ errmsg("enabling data checksums requested, starting data checksum calculation"));
+
+ /*
+ * Set the state to inprogress-on and wait on the procsignal barrier.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+ SetDataChecksumsOnInProgress();
+
+ /*
+ * All backends are now in inprogress-on state and are writing data
+ * checksums. Start processing all data at rest.
+ */
+ if (!ProcessAllDatabases())
+ {
+ /*
+ * If the target state changed during processing then it's not a
+ * failure, so restart processing instead.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto done;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("unable to enable data checksums in cluster"));
+ }
+
+ /*
+ * Data checksums have been set on all pages, set the state to on in
+ * order to instruct backends to validate checksums on reading.
+ */
+ SetDataChecksumsOn();
+
+ ereport(LOG,
+ errmsg("data checksums are now enabled"));
+ }
+ else if (operation == DISABLE_DATACHECKSUMS)
+ {
+ ereport(LOG,
+ errmsg("disabling data checksums requested"));
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+ SetDataChecksumsOff();
+ ereport(LOG,
+ errmsg("data checksums are now disabled"));
+ }
+ else
+ Assert(false);
+
+done:
+
+ /*
+ * This state will only be displayed for a fleeting moment, but for the
+ * sake of correctness it is still added before ending the command.
+ */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+ /*
+ * All done. But before we exit, check if the target state was changed
+ * while we were running. In that case we will have to start all over
+ * again.
+ */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if (DataChecksumState->launch_operation != operation)
+ {
+ DataChecksumState->operation = DataChecksumState->launch_operation;
+ operation = DataChecksumState->launch_operation;
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ LWLockRelease(DataChecksumsWorkerLock);
+ goto again;
+ }
+
+ /* Shut down progress reporting as we are done */
+ pgstat_progress_end_command();
+
+ launcher_running = false;
+ DataChecksumState->launcher_running = false;
+ LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ * Compute the list of all databases and process checksums in each
+ *
+ * This will generate a list of databases to process for enabling checksums.
+ * If a database encounters a failure then processing will end immediately and
+ * return an error.
+ */
+static bool
+ProcessAllDatabases(void)
+{
+ List *DatabaseList;
+ int cumulative_total = 0;
+
+ /* Set up so first run processes shared catalogs, not once in every db */
+ DataChecksumState->process_shared_catalogs = true;
+
+ /* Get a list of all databases to process */
+ WaitForAllTransactionsToFinish();
+ DatabaseList = BuildDatabaseList();
+
+ /*
+ * Update progress reporting with the total number of databases we need to
+ * process. This number should not be changed during processing, the
+ * columns for processed databases is instead increased such that it can
+ * be compared against the total.
+ */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+ PROGRESS_DATACHECKSUMS_DBS_DONE,
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE,
+ PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+ PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+ };
+
+ int64 vals[6];
+
+ vals[0] = list_length(DatabaseList);
+ vals[1] = 0;
+ /* translated to NULL */
+ vals[2] = -1;
+ vals[3] = -1;
+ vals[4] = -1;
+ vals[5] = -1;
+
+ pgstat_progress_update_multi_param(6, index, vals);
+ }
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+ {
+ DataChecksumsWorkerResult result;
+
+ result = ProcessDatabase(db);
+
+ /* Allow a test process to alter the result of the operation */
+ INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+ ++cumulative_total);
+
+ if (result == DATACHECKSUMSWORKER_FAILED)
+ {
+ /*
+ * Disable checksums on cluster, because we failed one of the
+ * databases and this is an all or nothing process.
+ */
+ SetDataChecksumsOff();
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+ errmsg("data checksums failed to get enabled in all databases, aborting"),
+ errhint("The server log might have more information on the cause of the error."));
+ }
+ else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested)
+ {
+ /* Abort flag set, so exit the whole process */
+ return false;
+ }
+
+ /*
+ * When one database has completed, it will have done shared catalogs
+ * so we don't have to process them again.
+ */
+ DataChecksumState->process_shared_catalogs = false;
+ }
+
+ FreeDatabaseList(DatabaseList);
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+ return true;
+}
+
+/*
+ * DataChecksumStateSize
+ * Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsShmemSize(void)
+{
+ Size size;
+
+ size = sizeof(DataChecksumsStateStruct);
+ size = MAXALIGN(size);
+
+ return size;
+}
+
+/*
+ * DataChecksumStateInit
+ * Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsShmemInit(void)
+{
+ bool found;
+
+ DataChecksumState = (DataChecksumsStateStruct *)
+ ShmemInitStruct("DataChecksumsWorker Data",
+ DataChecksumsShmemSize(),
+ &found);
+ if (!found)
+ MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
+}
+
+/*
+ * DatabaseExists
+ *
+ * Scans the system catalog to check if a database with the given Oid exist
+ * and returns true if it is found, else false.
+ */
+static bool
+DatabaseExists(Oid dboid)
+{
+ Relation rel;
+ ScanKeyData skey;
+ SysScanDesc scan;
+ bool found;
+ HeapTuple tuple;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_database_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ dboid);
+ scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf,
+ 1, &skey);
+ tuple = systable_getnext(scan);
+ found = HeapTupleIsValid(tuple);
+
+ systable_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return found;
+}
+
+/*
+ * BuildDatabaseList
+ * Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+ List *DatabaseList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(DatabaseRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+ DataChecksumsWorkerDatabase *db;
+
+ oldctx = MemoryContextSwitchTo(ctx);
+
+ db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+ db->dboid = pgdb->oid;
+ db->dbname = pstrdup(NameStr(pgdb->datname));
+
+ DatabaseList = lappend(DatabaseList, db);
+
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+ if (!dblist)
+ return;
+
+ foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+ {
+ if (db->dbname != NULL)
+ pfree(db->dbname);
+ }
+
+ list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ * Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request relation types. If temp_relations
+ * is True then only temporary relations are returned. If temp_relations is
+ * False then non-temporary relations which have data checksums are returned.
+ * If include_shared is True then shared relations are included as well in a
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+ List *RelationList = NIL;
+ Relation rel;
+ TableScanDesc scan;
+ HeapTuple tup;
+ MemoryContext ctx = CurrentMemoryContext;
+ MemoryContext oldctx;
+
+ StartTransactionCommand();
+
+ rel = table_open(RelationRelationId, AccessShareLock);
+ scan = table_beginscan_catalog(rel, 0, NULL);
+
+ while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+ {
+ Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+ /* Only include temporary relations when explicitly asked to */
+ if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+ {
+ if (!temp_relations)
+ continue;
+ }
+ else
+ {
+ /*
+ * If we are only interested in temp relations then continue
+ * immediately as the current relation isn't a temp relation.
+ */
+ if (temp_relations)
+ continue;
+
+ if (!RELKIND_HAS_STORAGE(pgc->relkind))
+ continue;
+
+ if (pgc->relisshared && !include_shared)
+ continue;
+ }
+
+ oldctx = MemoryContextSwitchTo(ctx);
+ RelationList = lappend_oid(RelationList, pgc->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
+
+ table_endscan(scan);
+ table_close(rel, AccessShareLock);
+
+ CommitTransactionCommand();
+
+ return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+ Oid dboid = DatumGetObjectId(arg);
+ List *RelationList = NIL;
+ List *InitialTempTableList = NIL;
+ BufferAccessStrategy strategy;
+ bool aborted = false;
+ int64 rels_done;
+
+ operation = ENABLE_DATACHECKSUMS;
+
+ pqsignal(SIGTERM, die);
+ pqsignal(SIGUSR1, procsignal_sigusr1_handler);
+
+ BackgroundWorkerUnblockSignals();
+
+ MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+ init_ps_display(NULL);
+
+ BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+ BGWORKER_BYPASS_ALLOWCONN);
+
+ /* worker will have a separate entry in pg_stat_progress_data_checksums */
+ pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+ InvalidOid);
+
+ /*
+ * Get a list of all temp tables present as we start in this database. We
+ * need to wait until they are all gone until we are done, since we cannot
+ * access these relations and modify them.
+ */
+ InitialTempTableList = BuildRelationList(true, false);
+
+ /*
+ * Enable vacuum cost delay, if any. While this process isn't doing any
+ * vacuuming, we are re-using the infrastructure that vacuum cost delay
+ * provides rather than inventing something bespoke. This is an internal
+ * implementation detail and care should be taken to avoid it bleeding
+ * through to the user to avoid confusion.
+ */
+ Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+ VacuumCostDelay = DataChecksumState->cost_delay;
+ VacuumCostLimit = DataChecksumState->cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumCostBalance = 0;
+ VacuumCostPageHit = 0;
+ VacuumCostPageMiss = 0;
+ VacuumCostPageDirty = 0;
+
+ /*
+ * Create and set the vacuum strategy as our buffer strategy.
+ */
+ strategy = GetAccessStrategy(BAS_VACUUM);
+
+ RelationList = BuildRelationList(false,
+ DataChecksumState->process_shared_catalogs);
+
+ /* Update the total number of relations to be processed in this DB. */
+ {
+ const int index[] = {
+ PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+ PROGRESS_DATACHECKSUMS_RELS_DONE
+ };
+
+ int64 vals[2];
+
+ vals[0] = list_length(RelationList);
+ vals[1] = 0;
+
+ pgstat_progress_update_multi_param(2, index, vals);
+ }
+
+ /* Process the relations */
+ rels_done = 0;
+ foreach_oid(reloid, RelationList)
+ {
+ CHECK_FOR_INTERRUPTS();
+
+ if (!ProcessSingleRelationByOid(reloid, strategy))
+ {
+ aborted = true;
+ break;
+ }
+
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+ ++rels_done);
+ }
+ list_free(RelationList);
+
+ if (aborted)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(DEBUG1,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+
+ /* The worker is about to wait for temporary tables to go away. */
+ pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+ PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+ /*
+ * Wait for all temp tables that existed when we started to go away. This
+ * is necessary since we cannot "reach" them to enable checksums. Any temp
+ * tables created after we started will already have checksums in them
+ * (due to the "inprogress-on" state), so no need to wait for those.
+ */
+ for (;;)
+ {
+ List *CurrentTempTables;
+ int numleft;
+ char activity[64];
+
+ CurrentTempTables = BuildRelationList(true, false);
+ numleft = 0;
+ foreach_oid(tmptbloid, InitialTempTableList)
+ {
+ if (list_member_oid(CurrentTempTables, tmptbloid))
+ numleft++;
+ }
+ list_free(CurrentTempTables);
+
+ INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+ if (numleft == 0)
+ break;
+
+ /*
+ * At least one temp table is left to wait for, indicate in pgstat
+ * activity and progress reporting.
+ */
+ snprintf(activity,
+ sizeof(activity),
+ "Waiting for %d temp tables to be removed", numleft);
+ pgstat_report_activity(STATE_RUNNING, activity);
+
+ /* Retry every 3 seconds */
+ ResetLatch(MyLatch);
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 3000,
+ WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ aborted = DataChecksumState->launch_operation != operation;
+ LWLockRelease(DataChecksumsWorkerLock);
+
+ CHECK_FOR_INTERRUPTS();
+
+ if (aborted || abort_requested)
+ {
+ DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ ereport(LOG,
+ errmsg("data checksum processing aborted in database OID %u",
+ dboid));
+ return;
+ }
+ }
+
+ list_free(InitialTempTableList);
+
+ /* worker done */
+ pgstat_progress_end_command();
+
+ DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..6cba23bbeef 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
'bgworker.c',
'bgwriter.c',
'checkpointer.c',
+ 'datachecksum_state.c',
'fork_process.c',
'interrupt.c',
'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index abf0c97569e..eb4f3eb72d4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2991,6 +2991,11 @@ PostmasterStateMachine(void)
B_INVALID,
B_STANDALONE_BACKEND);
+ /* also add data checksums processes */
+ remainMask = btmask_add(remainMask,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER);
+
/* All types should be included in targetMask or remainMask */
Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..57aaef57c61 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -189,6 +189,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
}
}
+void
+xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+ uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK;
+
+ ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr);
+
+ switch (info)
+ {
+ case XLOG2_CHECKSUMS:
+ break;
+ default:
+ elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info);
+ }
+}
+
/*
* Handle rmgr XACT_ID records for LogicalDecodingProcessRecord().
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c64570020d..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8567,6 +8567,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+ /*
+ * If the buffers are marked for zero on error, we want to log that in
+ * case of a checksum failure.
+ */
+ if (flags & READ_BUFFERS_ZERO_ON_ERROR)
+ piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR;
+
/* Check for garbage data. */
if (!failed)
{
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d692d419846..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
#include "postmaster/autovacuum.h"
#include "postmaster/bgworker_internals.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksum_state.h"
#include "postmaster/walsummarizer.h"
#include "replication/logicallauncher.h"
#include "replication/origin.h"
@@ -142,6 +143,7 @@ CalculateShmemSize(void)
size = add_size(size, AioShmemSize());
size = add_size(size, WaitLSNShmemSize());
size = add_size(size, LogicalDecodingCtlShmemSize());
+ size = add_size(size, DataChecksumsShmemSize());
/* include additional requested shmem from preload libraries */
size = add_size(size, total_addin_request);
@@ -310,6 +312,7 @@ CreateOrAttachShmemStructs(void)
PgArchShmemInit();
ApplyLauncherShmemInit();
SlotSyncShmemInit();
+ DataChecksumsShmemInit();
/*
* Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..f1ab3aa3fe0 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -22,6 +22,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
+#include "postmaster/datachecksum_state.h"
#include "replication/logicalctl.h"
#include "replication/logicalworker.h"
#include "replication/walsender.h"
@@ -582,6 +583,13 @@ ProcessProcSignalBarrier(void)
case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
processed = ProcessBarrierUpdateXLogLogicalInfo();
break;
+
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+ case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+ case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+ processed = AbsorbDataChecksumsBarrier(type);
+ break;
}
/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
2010/12/22 on -hackers list.
Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster. Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
The checksum is not valid at all times on a data page!!
The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 56f1f7ae9fc..1fdfda59edd 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*/
if (!PageIsNew(page))
{
- if (DataChecksumsEnabled())
+ /*
+ * There shouldn't be any check for interrupt calls happening in this
+ * codepath, but just to be on the safe side we hold interrupts since
+ * if they did happen the data checksum state could change during
+ * verifying checksums, which could lead to incorrect verification
+ * results.
+ */
+ HOLD_INTERRUPTS();
+ if (DataChecksumsNeedVerify())
{
checksum = pg_checksum_page(page, blkno);
@@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
*checksum_failure_p = true;
}
}
+ RESUME_INTERRUPTS();
/*
* The following checks don't prove the header is correct, only that
@@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("page verification failed, calculated checksum %u but expected %u",
- checksum, p->pd_checksum)));
+ errmsg("page verification failed, calculated checksum %u but expected %u%s",
+ checksum, p->pd_checksum,
+ (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : ""))));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
@@ -1507,9 +1517,14 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
void
PageSetChecksum(Page page, BlockNumber blkno)
{
+ HOLD_INTERRUPTS();
/* If we don't need a checksum, just return */
- if (PageIsNew(page) || !DataChecksumsEnabled())
+ if (PageIsNew(page) || !DataChecksumsNeedWrite())
+ {
+ RESUME_INTERRUPTS();
return;
+ }
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+ RESUME_INTERRUPTS();
}
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 7727fed3bda..04fe13e64c6 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
case B_CHECKPOINTER:
case B_IO_WORKER:
case B_STARTUP:
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
return false;
case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
case B_LOGGER:
return false;
+ case B_DATACHECKSUMSWORKER_LAUNCHER:
+ case B_DATACHECKSUMSWORKER_WORKER:
case B_AUTOVAC_LAUNCHER:
case B_AUTOVAC_WORKER:
case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..0a6d16f8154 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c
CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting."
CHECKPOINT_DONE "Waiting for a checkpoint to complete."
CHECKPOINT_START "Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled."
EXECUTE_GATHER "Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table."
HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -365,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."
+DataChecksumsWorker "Waiting for data checksums worker."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..1408de387ea 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -297,6 +297,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
cmdtype = PROGRESS_COMMAND_BASEBACKUP;
else if (pg_strcasecmp(cmd, "COPY") == 0)
cmdtype = PROGRESS_COMMAND_COPY;
+ else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+ cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1182,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
int64 result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
@@ -1200,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
TimestampTz result;
PgStat_StatDBEntry *dbentry;
- if (!DataChecksumsEnabled())
- PG_RETURN_NULL();
-
if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
result = 0;
else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ba191977697..7ffc808073a 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -845,7 +845,8 @@ InitializeSessionUserIdStandalone(void)
* workers, in slot sync worker and in background workers.
*/
Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
- AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+ AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+ AmDataChecksumsWorkerProcess());
/* call only once */
Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 783a7400464..6f074013aa9 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -758,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
+ /*
+ * Initialize a local cache of the data_checksum_version, to be updated by
+ * the procsignal-based barriers.
+ *
+ * This intentionally happens after initializing the procsignal, otherwise
+ * we might miss a state change. This means we can get a barrier for the
+ * state we've just initialized.
+ *
+ * The postmaster (which is what gets forked into the new child process)
+ * does not handle barriers, therefore it may not have the current value
+ * of LocalDataChecksumVersion value (it'll have the value read from the
+ * control file, which may be arbitrarily old).
+ *
+ * NB: Even if the postmaster handled barriers, the value might still be
+ * stale, as it might have changed after this process forked.
+ */
+ InitLocalDataChecksumState();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
@@ -886,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
username != NULL ? username : "postgres")));
}
- else if (AmBackgroundWorkerProcess())
+ else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
{
if (username == NULL && !OidIsValid(useroid))
{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index fc0900efe5f..a315c4ab8ab 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -571,11 +571,12 @@
max => '1.0',
},
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
short_desc => 'Shows whether data checksums are turned on for this cluster.',
flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
variable => 'data_checksums',
- boot_val => 'false',
+ boot_val => 'PG_DATA_CHECKSUM_OFF',
+ options => 'data_checksums_options',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e14b7b4af0..d9ca13baff9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -501,6 +501,14 @@ static const struct config_enum_entry file_extend_method_options[] = {
{NULL, 0, false}
};
+static const struct config_enum_entry data_checksums_options[] = {
+ {"on", PG_DATA_CHECKSUM_VERSION, true},
+ {"off", PG_DATA_CHECKSUM_OFF, true},
+ {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true},
+ {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true},
+ {NULL, 0, false}
+};
+
/*
* Options for enum values stored in other modules
*/
@@ -629,7 +637,6 @@ static int shared_memory_size_in_huge_pages;
static int wal_block_size;
static int num_os_semaphores;
static int effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
static bool integer_datetimes;
#ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c8194c27aa7..6d0337853e0 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -543,11 +543,11 @@
# archiver autovacuum
# backend bgworker
# bgwriter checkpointer
- # ioworker postmaster
- # slotsyncworker startup
- # syslogger walreceiver
- # walsummarizer walwriter
- # walsender
+ # checksums ioworker
+ # postmaster slotsyncworker
+ # startup syslogger
+ # walreceiver walsummarizer
+ # walwriter walsender
#
# Level values in order of decreasing
# detail:
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
pg_fatal("cluster must be shut down");
- if (ControlFile->data_checksum_version == 0 &&
+ if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_CHECK)
pg_fatal("data checksums are not enabled in cluster");
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
mode == PG_MODE_DISABLE)
pg_fatal("data checksums are already disabled in cluster");
- if (ControlFile->data_checksum_version > 0 &&
+ if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
mode == PG_MODE_ENABLE)
pg_fatal("data checksums are already enabled in cluster");
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..fe5fc5ec133 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
ControlFile->checkPointCopy.oldestCommitTsXid);
printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
ControlFile->checkPointCopy.newestCommitTsXid);
+ printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+ ControlFile->checkPointCopy.dataChecksumState);
printf(_("Time of latest checkpoint: %s\n"),
ckpttime_str);
printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..79053d22dcc 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
#include "access/xlog_internal.h"
#include "common/string.h"
#include "pg_upgrade.h"
+#include "storage/checksum.h"
/*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
* check_for_isn_and_int8_passing_mismatch().
*/
+ /*
+ * If data checksums are in any in-progress state then disallow the
+ * upgrade. The user should either let the process finish, or turn off
+ * data checksums, before retrying.
+ */
+ if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+ pg_fatal("checksums are being enabled in the old cluster");
+
/*
* We might eventually allow upgrades from checksum to no-checksum
* clusters.
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index a268f0f1dd0..7dd1c3dd63e 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -79,7 +79,8 @@ BRIN
CommitTs
ReplicationOrigin
Generic
-LogicalMessage$/,
+LogicalMessage
+XLOG2$/,
'rmgr list');
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index 3352b5f8532..ae32ef16d67 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL)
PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL)
PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode)
+PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode)
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index dcc12eb8cbe..4af38e74ce4 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
extern PGDLLIMPORT int CommitSiblings;
extern PGDLLIMPORT bool track_wal_io_timing;
extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
extern PGDLLIMPORT int CheckPointSegments;
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
* of the bits make it to disk, but the checksum wouldn't match. Also WAL-log
* them if forced by wal_log_hints=on.
*/
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
/* Do we need to WAL-log information required only for Hot Standby and logical replication? */
#define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -229,8 +230,11 @@ extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn);
extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
extern void xlog_redo(struct XLogReaderState *record);
+extern void xlog2_redo(struct XLogReaderState *record);
extern void xlog_desc(StringInfo buf, struct XLogReaderState *record);
+extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record);
extern const char *xlog_identify(uint8 info);
+extern const char *xlog2_identify(uint8 info);
extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli);
@@ -243,7 +247,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsInProgressOn(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern const char *show_data_checksums(void);
+extern const char *get_checksum_state_string(uint32 state);
+extern void InitLocalDataChecksumState(void);
+extern void SetLocalDataChecksumState(uint32 data_checksum_version);
extern bool GetDefaultCharSignedness(void);
extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 755835d63bf..10c18d39ff8 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
#include "lib/stringinfo.h"
#include "pgtime.h"
#include "storage/block.h"
+#include "storage/checksum.h"
#include "storage/relfilelocator.h"
@@ -287,6 +288,12 @@ typedef struct xl_restore_point
char rp_name[MAXFNAMELEN];
} xl_restore_point;
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+ ChecksumStateType new_checksum_state;
+} xl_checksum_state;
+
/* Overwrite of prior contrecord */
typedef struct xl_overwrite_contrecord
{
@@ -307,6 +314,7 @@ typedef struct xl_end_of_recovery
typedef struct xl_checkpoint_redo
{
int wal_level;
+ uint32 data_checksum_version;
} xl_checkpoint_redo;
/*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index b1c5afc15df..582bb2e2058 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202604021
+#define CATALOG_VERSION_NO 202604031
#endif
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 77a661e818b..80b3a730e03 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
/* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION 1901
+#define PG_CONTROL_VERSION 1902
/* Nonce key length, see below */
#define MOCK_AUTH_NONCE_LEN 32
@@ -63,6 +63,9 @@ typedef struct CheckPoint
* set to InvalidTransactionId.
*/
TransactionId oldestActiveXid;
+
+ /* data checksums state at the time of the checkpoint */
+ uint32 dataChecksumState;
} CheckPoint;
/* XLOG info values for XLOG rmgr */
@@ -83,6 +86,9 @@ typedef struct CheckPoint
#define XLOG_CHECKPOINT_REDO 0xE0
#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0
+/* XLOG info values for XLOG2 rmgr */
+#define XLOG2_CHECKSUMS 0x00
+
/*
* System status indicator. Note this is stored in pg_control; if you change
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acf16254b21..bd177aebfcb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12558,6 +12558,20 @@
proname => 'jsonb_subscript_handler', prorettype => 'internal',
proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
+# data checksum management functions
+{ oid => '9258',
+ descr => 'disable data checksums',
+ proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '',
+ proacl => '{POSTGRES=X}'},
+{ oid => '9257',
+ descr => 'enable data checksums',
+ proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+ proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+ proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+ proargdefaults => '{0,100}', prosrc => 'enable_data_checksums',
+ proacl => '{POSTGRES=X}'},
+
# collation management functions
{ oid => '3445', descr => 'import collations from operating system',
proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..67948667a97 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -185,4 +185,20 @@
#define PROGRESS_COPY_TYPE_PIPE 3
#define PROGRESS_COPY_TYPE_CALLBACK 4
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE 0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE 2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE 4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4
+
#endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 04f29748be7..7277c37e779 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
B_WAL_SUMMARIZER,
B_WAL_WRITER,
+ B_DATACHECKSUMSWORKER_LAUNCHER,
+ B_DATACHECKSUMSWORKER_WORKER,
+
/*
* Logger is not connected to shared memory and does not have a PGPROC
* entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
#define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER)
#define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER)
#define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+ (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+ MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
#define AmSpecialWorkerProcess() \
(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
new file mode 100644
index 00000000000..343494edcc8
--- /dev/null
+++ b/src/include/postmaster/datachecksum_state.h
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksum_state.h
+ * header file for data checksum helper background worker and data
+ * checksum state manipulation
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksum_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUM_STATE_H
+#define DATACHECKSUM_STATE_H
+
+#include "storage/procsignal.h"
+
+/* Shared memory */
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+ ENABLE_DATACHECKSUMS,
+ DISABLE_DATACHECKSUMS,
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+ DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+ DATACHECKSUMSWORKER_ABORTED,
+ DATACHECKSUMSWORKER_FAILED,
+ DATACHECKSUMSWORKER_DROPDB,
+} DataChecksumsWorkerResult;
+
+/* Prototypes for data checksum state manipulation */
+bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType target_state);
+void EmitAndWaitDataChecksumsBarrier(uint32 state);
+
+/* Prototypes for data checksum background worker */
+
+/* Start the background processes for enabling or disabling checksums */
+void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+ int cost_delay,
+ int cost_limit);
+
+/* Background worker entrypoints */
+void DataChecksumsWorkerLauncherMain(Datum arg);
+void DataChecksumsWorkerMain(Datum arg);
+
+#endif /* DATACHECKSUM_STATE_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index feac19ba207..b3477e6f17a 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, t
PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true)
PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true)
PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false)
PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true)
PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false)
PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..107e43ef750 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer
} XLogRecordBuffer;
extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index e5267b93fe6..634e1e49ee5 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -230,7 +230,6 @@ typedef PageHeaderData *PageHeader;
* handling pages.
*/
#define PG_PAGE_LAYOUT_VERSION 4
-#define PG_DATA_CHECKSUM_VERSION 1
/* ----------------------------------------------------------------
* page support functions
@@ -501,6 +500,7 @@ do { \
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)
+#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..3b1440c0c95 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,22 @@
#include "storage/block.h"
+/*
+ * Checksum state 0 is used for when data checksums are disabled (OFF).
+ * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
+ * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
+ * that data checksums are enabled. The ChecksumStateType is stored in
+ * pg_control so changing requires a catversion bump, and the values cannot
+ * be reordered. New states must be added at the end.
+ */
+typedef enum ChecksumStateType
+{
+ PG_DATA_CHECKSUM_OFF = 0,
+ PG_DATA_CHECKSUM_VERSION = 1,
+ PG_DATA_CHECKSUM_INPROGRESS_OFF = 2,
+ PG_DATA_CHECKSUM_INPROGRESS_ON = 3,
+} ChecksumStateType;
+
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 59ee097977d..af8553bcb6c 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, DataChecksumsWorker)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..cc4f26aa33d 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -48,6 +48,10 @@ typedef enum
PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update
* XLogLogicalInfo */
+ PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+ PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+ PROCSIGNAL_BARRIER_CHECKSUM_ON,
} ProcSignalBarrierType;
/*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 6300dbd15d5..61e13c40e28 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
PROGRESS_COMMAND_BASEBACKUP,
PROGRESS_COMMAND_COPY,
PROGRESS_COMMAND_REPACK,
+ PROGRESS_COMMAND_DATACHECKSUMS,
} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 28ce3b35eda..864b407abcf 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
test_bitmapset \
test_bloomfilter \
test_cloexec \
+ test_checksums \
test_copy_callbacks \
test_custom_rmgrs \
test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 3ac291656c1..e5acacd5083 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
subdir('test_bitmapset')
subdir('test_bloomfilter')
subdir('test_cloexec')
+subdir('test_checksums')
subdir('test_copy_callbacks')
subdir('test_cplusplusext')
subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+ $(WIN32RES) \
+ test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+ make check
+
+or
+
+ make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+ meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function. In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..9b1421a9b91
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+ 'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+ test_checksums_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+ 'test_checksums.control',
+ 'test_checksums--1.0.sql',
+)
+
+tests += {
+ 'name': 'test_checksums',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+ },
+ 'tests': [
+ 't/001_basic.pl',
+ 't/002_restarts.pl',
+ 't/003_standby_restarts.pl',
+ 't/004_offline.pl',
+ 't/005_injection.pl',
+ 't/006_pgbench_single.pl',
+ 't/007_pgbench_standby.pl',
+ 't/008_pitr.pl',
+ 't/009_fpi.pl',
+ ],
+ },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..c008e95fbff
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('basic_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 1);
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..bab59be82bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks processing.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('restarts_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Create a barrier for checksum enablement to block on, in this case a pre-
+ # existing temporary table which is kept open while processing is started.
+ # We can accomplish this by setting up an interactive psql process which
+ # keeps the temporary table created as we enable checksums in another psql
+ # process.
+ #
+ # This is a similar test to the synthetic variant in 005_injection.pl
+ # which fakes this scenario.
+ my $bsession = $node->background_psql('postgres');
+ $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+ # In another session, make sure we can see the blocking temp table but
+ # start processing anyways and check that we are blocked with a proper
+ # wait event.
+ $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+ );
+ is($result, 't', 'ensure we can see the temporary table');
+
+ # Enabling data checksums shouldn't work as the process is blocked on the
+ # temporary table held open by $bsession. Ensure that we reach inprogress-
+ # on before we do more tests.
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ # Wait for processing to finish and the worker waiting for leftover temp
+ # relations to be able to actually finish
+ $result = $node->poll_query_until(
+ 'postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';",
+ 'ChecksumEnableTemptableWait');
+
+ # The datachecksumsworker waits for temporary tables to disappear for 3
+ # seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+ # a retry cycle
+ sleep(4);
+
+ # Re-check the wait event to ensure we are blocked on the right thing.
+ $result = $node->safe_psql('postgres',
+ "SELECT wait_event FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum worker';");
+ is($result, 'ChecksumEnableTemptableWait',
+ 'ensure the correct wait condition is set');
+ test_checksum_state($node, 'inprogress-on');
+
+ # Stop the cluster while bsession is still attached. We can't close the
+ # session first since the brief period between closing and stopping might
+ # be enough for checksums to get enabled.
+ $node->stop;
+ $bsession->quit;
+ $node->start;
+
+ # Ensure the checksums aren't enabled across the restart. This leaves the
+ # cluster in the same state as before we entered the SKIP block.
+ test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$result = $node->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..6b016925651
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on". Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ 'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+ "INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, '19998', 'ensure we can safely read all data with checksums');
+
+$result = $node_primary->poll_query_until(
+ 'postgres',
+ "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
+ '0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby, 'off');
+# Doublecheck reading data without errors
+$result =
+ $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f1972bddff1
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum processing
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('offline_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksum enablement to block on, in this case a pre-
+# existing temporary table which is kept open while processing is started. We
+# can accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..897f282a1f2
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,74 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+SKIP:
+{
+ skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+ if (!$ENV{PG_TEST_EXTRA}
+ || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+ # Inject a delay in the barrier for enabling checksums
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+ enable_data_checksums($node, wait => 'on');
+
+ # Fake the existence of a temporary table at the start of processing, which
+ # will force the processing to wait and retry in order to wait for it to
+ # disappear.
+ disable_data_checksums($node, wait => 1);
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ enable_data_checksums($node, wait => 'on');
+}
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..0ab5b04b931
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,275 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 10 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksums enabled on the primary
+ wait_for_checksum_state($node, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('pgbench_single_main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+my $scalefactor = ($extended ? 10 : 1);
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node->is_alive)
+ {
+ # Start, to do recovery, and stop
+ $node->start;
+ $node->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+
+ # Randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 64 + int(rand(1024));
+ $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+ note("changing max_wal_size to " . $sb);
+
+ $node->start;
+
+ # Start a pgbench in the background against the primary
+ background_rw_pgbench($node->port);
+ }
+
+ $node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+
+ random_sleep() if ($extended);
+
+ # Potentially powercycle the node
+ if (cointoss())
+ {
+ $node->stop(stopmode());
+
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node->data_dir);
+
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+ }
+
+ random_sleep() if ($extended);
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+ $node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..b0d40d24005
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,400 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby;
+my $node_standby_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+$TEST_ITERATIONS = 1 if ($extended);
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+ my ($port, $standby) = @_;
+ my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary);
+
+ # Terminate any currently running pgbench process before continuing
+ $$pgbench->finish if $$pgbench;
+
+ my $clients = 1;
+ my $runtime = 5;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # If we run on a standby it needs to be a read-only benchmark
+ push(@cmd, '-S') if ($standby);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $$pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+ test_checksum_state($node_standby, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # Coin-toss to see if we are injecting a retry due to a temptable
+ $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+ if cointoss();
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $result . "\n");
+
+ # Ensure that the primary switches to "inprogress-on"
+ enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+ random_sleep() if ($extended);
+
+ # Wait for checksum enable to be replayed
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Ensure that the standby has switched to "inprogress-on" or "on".
+ # Normally it would be "inprogress-on", but it is theoretically
+ # possible for the primary to complete the checksum enabling *and* have
+ # the standby replay that record before we reach the check below.
+ $result = $node_standby->poll_query_until(
+ 'postgres',
+ "SELECT setting = 'off' "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';",
+ 'f');
+ is($result, 1,
+ 'ensure standby has absorbed the inprogress-on barrier');
+ $result = $node_standby->safe_psql('postgres',
+ "SELECT setting "
+ . "FROM pg_catalog.pg_settings "
+ . "WHERE name = 'data_checksums';");
+
+ is(($result eq 'inprogress-on' || $result eq 'on'),
+ 1, 'ensure checksums are on, or in progress, on standby_1');
+
+ # Wait for checksums enabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'on');
+
+ $node_primary->safe_psql('postgres',
+ 'SELECT dcw_fake_temptable(false);');
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ random_sleep() if ($extended);
+
+ # log LSN right before we start changing checksums
+ my $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before disabling: " . $result . "\n");
+
+ disable_data_checksums($node_primary);
+ $node_primary->wait_for_catchup($node_standby, 'replay');
+
+ # Wait for checksums disabled on the primary and standby
+ wait_for_checksum_state($node_primary, 'off');
+ wait_for_checksum_state($node_standby, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $result =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after disabling: " . $result . "\n");
+
+ random_sleep() if ($extended);
+ wait_for_checksum_state($node_standby, 'off');
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby');
+$node_standby->init_from_backup($node_primary, $node_primary_backup,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state. The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+ note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+ if (!$node_primary->is_alive)
+ {
+ # start, to do recovery, and stop
+ $node_primary->start;
+ $node_primary->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing primary max_wal_size to " . $sb);
+
+ $node_primary->start;
+
+ # Start a pgbench in the background against the primary
+ background_pgbench($node_primary->port, 0);
+ }
+
+ if (!$node_standby->is_alive)
+ {
+ $node_standby->start;
+ $node_standby->stop('fast');
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (during WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+
+ # randomize the WAL size, to trigger checkpoints less/more often
+ my $sb = 32 + int(rand(960));
+ $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+ note("changing standby max_wal_size to " . $sb);
+
+ $node_standby->start;
+
+ # Start a read-only pgbench in the background on the standby
+ background_pgbench($node_standby->port, 1);
+ }
+
+ $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+ $node_primary->wait_for_catchup($node_standby, 'write');
+
+ flip_data_checksums();
+ random_sleep() if ($extended);
+ my $result = $node_primary->safe_psql('postgres',
+ "SELECT count(*) FROM t WHERE a > 1");
+ is($result, '100000', 'ensure data pages can be read back on primary');
+ random_sleep();
+
+ # Potentially powercycle the cluster (the nodes independently). A TODO is
+ # to randomly stop the nodes in the opposite order too.
+ if ($extended && cointoss())
+ {
+ $node_primary->stop(stopmode());
+
+ # print the contents of the control file on the primary
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_primary->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (outside WAL recovery)"
+ );
+ $node_primary_loglocation = -s $node_primary->logfile;
+ }
+
+ random_sleep() if ($extended);
+
+ if ($extended && cointoss())
+ {
+ $node_standby->stop(stopmode());
+
+ # print the contents of the control file on the standby
+ PostgreSQL::Test::Utils::system_log("pg_controldata",
+ $node_standby->data_dir);
+
+ # slurp the file after shutdown, so that it doesn't interfere with the recovery
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log (outside WAL recovery)"
+ );
+ $node_standby_loglocation = -s $node_standby->logfile;
+ }
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+ $node_primary->start;
+}
+
+if (!$node_standby->is_alive)
+{
+ $node_standby->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+ $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+ $node_primary_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
+ $node_standby_loglocation);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in standby_1 log");
+$node_standby_loglocation = -s $node_standby->logfile;
+
+$node_standby->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
new file mode 100644
index 00000000000..b9b89f414ab
--- /dev/null
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -0,0 +1,189 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+else
+{
+ plan skip_all => 'Expensive data checksums test disabled';
+}
+
+
+my $pgbench = undef;
+my $data_checksum_state = 'off';
+
+my $node_primary;
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+ my $lsn_pre = undef;
+ my $lsn_post = undef;
+
+ # First, make sure the cluster is in the state we expect it to be
+ test_checksum_state($node_primary, $data_checksum_state);
+
+ if ($data_checksum_state eq 'off')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN before enabling: " . $lsn_pre . "\n");
+
+ # Wait for checksums enabled on the primary
+ enable_data_checksums($node_primary, wait => 'on');
+
+ # log LSN right after the primary flips checksums to "on"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+ note("LSN after enabling: " . $lsn_post . "\n");
+
+ $data_checksum_state = 'on';
+ }
+ elsif ($data_checksum_state eq 'on')
+ {
+ # log LSN right before we start changing checksums
+ $lsn_pre =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ disable_data_checksums($node_primary);
+
+ # Wait for checksums disabled on the primary
+ wait_for_checksum_state($node_primary, 'off');
+
+ # log LSN right after the primary flips checksums to "off"
+ $lsn_post =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
+ $data_checksum_state = 'off';
+ }
+ else
+ {
+ # This should only happen due to programmer error when hacking on the
+ # test code, but since that might pass subtly we error out.
+ BAIL_OUT('data_checksum_state variable has invalid state:'
+ . $data_checksum_state);
+ }
+
+ return ($lsn_pre, $lsn_post);
+}
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ # Randomize the number of pgbench clients in extended mode, else 1 client
+ my $clients = ($extended ? 1 + int(rand(15)) : 1);
+ my $runtime = ($extended ? 600 : 5);
+
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss());
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a primary node with WAL archiving enabled and with enough connections
+# available to handle pgbench clients.
+$node_primary = PostgreSQL::Test::Cluster->new('pitr_main');
+$node_primary->init(
+ has_archiving => 1,
+ allows_streaming => 1,
+ no_data_checksums => 1);
+$node_primary->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node_primary->start;
+
+# Prime the cluster with a bit of known data which we can read back to check
+# for data consistency as well as page verification faults in the logfile.
+$node_primary->safe_psql('postgres',
+ 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;');
+# Initialize and start pgbench in read/write mode against the cluster
+my $scalefactor = ($extended ? 10 : 1);
+$node_primary->command_ok(
+ [
+ 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q',
+ 'postgres'
+ ]);
+background_rw_pgbench($node_primary->port);
+
+# Take a backup to use for PITR
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+my ($pre_lsn, $post_lsn) = flip_data_checksums();
+
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
+$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$node_primary->stop('immediate');
+
+my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
+$node_pitr->init_from_backup(
+ $node_primary, $backup_name,
+ standby => 0,
+ has_restoring => 1);
+$node_pitr->append_conf(
+ 'postgresql.conf', qq{
+recovery_target_lsn = '$post_lsn'
+recovery_target_action = 'promote'
+recovery_target_inclusive = on
+});
+
+$node_pitr->start;
+
+$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
+ or die "Timed out while waiting for PITR promotion";
+
+test_checksum_state($node_pitr, $data_checksum_state);
+my $result =
+ $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '99999', 'ensure data pages can be read back on primary');
+
+$node_pitr->stop;
+
+my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in pitr log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
new file mode 100644
index 00000000000..a1cea91f787
--- /dev/null
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Create and start a cluster with one node
+my $node = PostgreSQL::Test::Cluster->new('fpi_node');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+ "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;");
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+
+disable_data_checksums($node, wait => 1);
+
+$node->append_conf('postgresql.conf', 'full_page_writes = off');
+$node->restart;
+test_checksum_state($node, 'off');
+
+$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;');
+$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;');
+
+$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on');
+$node->restart;
+test_checksum_state($node, 'off');
+
+enable_data_checksums($node, wait => 'on');
+
+my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;');
+is($result, '990003', 'Reading back all data from table t');
+
+$node->stop;
+my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in server log");
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..9a2269e8a92
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,262 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use DataChecksums::Utils qw( .. );
+
+ # Create, and start, a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+ $node->init;
+ $node->start;
+
+ test_checksum_state($node, 'off');
+
+ enable_data_checksums($node);
+
+ wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+ cointoss
+ disable_data_checksums
+ enable_data_checksums
+ random_sleep
+ stopmode
+ test_checksum_state
+ wait_for_checksum_state
+ wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>. If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $result = $postgresnode->safe_psql('postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+ );
+ is($result, $state, 'ensure checksums are set to ' . $state);
+ return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out. Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+ my ($postgresnode, $state) = @_;
+
+ my $res = $postgresnode->poll_query_until(
+ 'postgres',
+ "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+ $state);
+ is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> responds to connections
+and return when it no longer does so, or when it times out. Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter. Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+ my $postgresnode = shift;
+ my %params = @_;
+ my $crash = 0;
+
+ $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+ unless (defined($params{timeout}));
+
+ for (my $naps = 0; $naps < $params{timeout}; $naps++)
+ {
+ if (!$postgresnode->is_alive)
+ {
+ $crash = 1;
+ last;
+ }
+ sleep(1);
+ }
+
+ return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+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.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ # Set sane defaults for the parameters
+ $params{cost_delay} = 0 unless (defined($params{cost_delay}));
+ $params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+ my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+ $postgresnode->safe_psql('postgres',
+ sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+ wait_for_checksum_state($postgresnode, $params{wait})
+ if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+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.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+ my $postgresnode = shift;
+ my %params = @_;
+
+ $postgresnode->safe_psql('postgres',
+ 'SELECT pg_disable_data_checksums();');
+
+ wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+ return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+ my $max = shift;
+ return if (defined($max) && ($max == 0));
+ sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+ return 'immediate' if (cointoss);
+ return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..90642d247fa
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,24 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+ RETURNS pg_catalog.void
+ AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..b087a3b4664
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,184 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ * Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksum_state.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+ (void) name;
+ (void) private_data;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ (3 * 1000),
+ WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksums-enable-checksums-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-launcher-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-startup-delay",
+ "test_checksums",
+ "dc_delay_barrier",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
+
+void
+dc_modify_db_result(const char *name, const void *private_data, void *arg)
+{
+ DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+ uint32 new_res = *(uint32 *) private_data;
+
+ *res = new_res;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-modify-db-result",
+ "test_checksums",
+ "dc_modify_db_result",
+ &db_fail,
+ sizeof(uint32));
+ else
+ InjectionPointDetach("datachecksumsworker-modify-db-result");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+ static bool first_pass = true;
+ int *numleft = (int *) arg;
+
+ if (first_pass)
+ *numleft = 1;
+ first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+ bool attach = PG_GETARG_BOOL(0);
+
+ if (attach)
+ InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+ "test_checksums",
+ "dc_fake_temptable",
+ NULL,
+ 0);
+ else
+ InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+ elog(ERROR,
+ "test is not working as intended when injection points are disabled");
+#endif
+ PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+ abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index f8dc732e66e..54e6b646e8f 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3898,6 +3898,42 @@ sub advance_wal
}
}
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+ my ($self) = @_;
+
+ print "# Enabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-e');
+ return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+ my ($self) = @_;
+
+ print "# Disabling checksums in \"$self->data_dir\"\n";
+ PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+ $self->data_dir, '-d');
+ return;
+}
+
=pod
=back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..81a73c426d2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2085,6 +2085,41 @@ pg_stat_progress_create_index| SELECT s.pid,
s.param15 AS partitions_done
FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+ s.datid,
+ d.datname,
+ CASE s.param1
+ WHEN 0 THEN 'enabling'::text
+ WHEN 1 THEN 'disabling'::text
+ WHEN 2 THEN 'waiting on temporary tables'::text
+ WHEN 3 THEN 'waiting on barrier'::text
+ WHEN 4 THEN 'done'::text
+ ELSE NULL::text
+ END AS phase,
+ CASE s.param2
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param2
+ END AS databases_total,
+ s.param3 AS databases_done,
+ CASE s.param4
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param4
+ END AS relations_total,
+ CASE s.param5
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param5
+ END AS relations_done,
+ CASE s.param6
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param6
+ END AS blocks_total,
+ CASE s.param7
+ WHEN '-1'::integer THEN NULL::bigint
+ ELSE s.param7
+ END AS blocks_done
+ FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+ LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+ ORDER BY s.datid;
pg_stat_progress_repack| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..35632f83052 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
client backend|temp relation|normal
client backend|wal|init
client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
io worker|relation|bulkread
io worker|relation|bulkwrite
io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(79 rows)
+(95 rows)
\a
-- ensure that both seqscan and indexscan plans are allowed
SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 91b1225da82..ad999aa48dd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -438,6 +438,8 @@ CheckPointStmt
CheckpointStatsData
CheckpointerRequest
CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumStateType
Chromosome
CkptSortItem
CkptTsStatus
@@ -610,6 +612,7 @@ CustomScan
CustomScanMethods
CustomScanState
CycleCtr
+DataChecksumsWorkerOperation
DBState
DbOidName
DCHCacheEntry
@@ -628,6 +631,9 @@ DSMREntryType
DSMRegistryCtxStruct
DSMRegistryEntry
DWORD
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsStateStruct
DataDirSyncMethod
DataDumperPtr
DataPageDeleteStack
@@ -4405,6 +4411,7 @@ xl_btree_unlink_page
xl_btree_update
xl_btree_vacuum
xl_checkpoint_redo
+xl_checksum_state
xl_clog_truncate
xl_commit_ts_truncate
xl_dbase_create_file_copy_rec
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 21:46 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-03 21:46 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
After many more runs on CI I ended up pushing this version, and I see BF
members being angry due the test not waiting for the launcher to exit. I am
working on a fix right now.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-03 22:59 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-03 22:59 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email protected]> wrote:
>
> After many more runs on CI I ended up pushing this version, and I see BF
> members being angry due the test not waiting for the launcher to exit. I am
> working on a fix right now.
0036232ba8f seems to have made the failing animals slightly happier, I will
continue to monitor the buildfarm for other fallout.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-04 00:35 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-04 00:35 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 4 Apr 2026, at 00:59, Daniel Gustafsson <[email protected]> wrote:
>
>> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email protected]> wrote:
>>
>> After many more runs on CI I ended up pushing this version, and I see BF
>> members being angry due the test not waiting for the launcher to exit. I am
>> working on a fix right now.
>
> 0036232ba8f seems to have made the failing animals slightly happier, I will
> continue to monitor the buildfarm for other fallout.
The intermittent failure on kestrel implies timing similar to the one fixed in
0036232ba8fb28, a tentative fix is to make it part of waiting for an endstate
(on or off) to make sure the cluster is always in the right state for new
operations. Right now kestrel is the one which has been flapping, I'm waiting
a bit to see if more will follow and give further clues.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] 0001-Wait-for-launcher-exit-in-enable-disable-checksum-te.patch (3.2K, ../../[email protected]/2-0001-Wait-for-launcher-exit-in-enable-disable-checksum-te.patch)
download | inline diff:
From fd90e1739e17a0b62efe3aa1e4a20fb34befb183 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Sat, 4 Apr 2026 02:12:49 +0200
Subject: [PATCH] Wait for launcher exit in enable/disable checksum tests
When enabling or disabling data checksums in a test with waiting
for an end state (on or off), the test typically want to perform
more test against the cluster immediately. Make sure to wait for
the launcher to exit in these cases before returning in order to
know it can immediately be acted on.
This is a more generic implementation of 0036232ba8f since more
buildfarm animals are showing timing related failures.
---
.../modules/test_checksums/t/001_basic.pl | 6 +----
.../test_checksums/t/DataChecksums/Utils.pm | 24 ++++++++++++++++---
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
index 5933c730da1..a78118320d5 100644
--- a/src/test/modules/test_checksums/t/001_basic.pl
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -35,11 +35,7 @@ my $result =
is($result, '9999', 'ensure checksummed pages can be read back');
# Enable data checksums again which should be a no-op so we explicitly don't
-# wait for any state transition as none should happen here. Make sure to let
-# any running launcher finish in case it's still wrapping up.
-$result = $node->poll_query_until('postgres',
- "SELECT count(*) = 0 FROM pg_catalog.pg_stat_activity WHERE backend_type = 'datachecksum launcher';"
-);
+# wait for any state transition as none should happen here.
enable_data_checksums($node);
test_checksum_state($node, 'on');
# ..and make sure we can still read/write data
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
index 9a2269e8a92..6ee3169c737 100644
--- a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -175,8 +175,19 @@ EOQ
$postgresnode->safe_psql('postgres',
sprintf($query, $params{cost_delay}, $params{cost_limit}));
- wait_for_checksum_state($postgresnode, $params{wait})
- if (defined($params{wait}));
+ if (defined($params{wait}))
+ {
+ wait_for_checksum_state($postgresnode, $params{wait});
+ # If we are tasked with waiting for an end state, also wait for the
+ # launcher to exit.
+ if ($params{wait} eq 'on' || $params{wait} eq 'off')
+ {
+ $postgresnode->poll_query_until('postgres',
+ "SELECT count(*) = 0 ".
+ "FROM pg_catalog.pg_stat_activity " .
+ "WHERE backend_type = 'datachecksum launcher';");
+ }
+ }
}
=item disable_data_checksums($node, %params)
@@ -204,7 +215,14 @@ sub disable_data_checksums
$postgresnode->safe_psql('postgres',
'SELECT pg_disable_data_checksums();');
- wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+ if (defined($params{wait}))
+ {
+ wait_for_checksum_state($postgresnode, 'off');
+ $postgresnode->poll_query_until('postgres',
+ "SELECT count(*) = 0 ".
+ "FROM pg_catalog.pg_stat_activity " .
+ "WHERE backend_type = 'datachecksum launcher';");
+ }
}
=item cointoss
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-04 22:27 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-04 22:27 UTC (permalink / raw)
To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 4 Apr 2026, at 02:35, Daniel Gustafsson <[email protected]> wrote:
>
>> On 4 Apr 2026, at 00:59, Daniel Gustafsson <[email protected]> wrote:
>>
>>> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email protected]> wrote:
>>>
>>> After many more runs on CI I ended up pushing this version, and I see BF
>>> members being angry due the test not waiting for the launcher to exit. I am
>>> working on a fix right now.
>>
>> 0036232ba8f seems to have made the failing animals slightly happier, I will
>> continue to monitor the buildfarm for other fallout.
>
> The intermittent failure on kestrel implies timing similar to the one fixed in
> 0036232ba8fb28, a tentative fix is to make it part of waiting for an endstate
> (on or off) to make sure the cluster is always in the right state for new
> operations. Right now kestrel is the one which has been flapping, I'm waiting
> a bit to see if more will follow and give further clues.
mylodon had the same failure, and I believe the bug is in my injection point
test code. I have a tentative fix in the attached refactoring which moves over
to using the injection_point extension module. It's still fairly rare so I'm
holding off for a little bit before pushing it to see if I can collect a little
bit more evidence.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] 0002-Refactor-checksum-injection-point-tests.patch (10.8K, ../../[email protected]/2-0002-Refactor-checksum-injection-point-tests.patch)
download | inline diff:
From 968caa8cdd45252ec9f507dde3bff403f937cd21 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Sun, 5 Apr 2026 00:01:06 +0200
Subject: [PATCH 2/2] Refactor checksum injection point tests
Refactor the injection point tests for online checksum enabling to
use the injection_points extension.
Author: Daniel Gustafsson <[email protected]>
---
src/backend/postmaster/datachecksum_state.c | 26 +++++-
.../modules/test_checksums/t/005_injection.pl | 7 +-
.../test_checksums/t/006_pgbench_single.pl | 13 ++-
.../test_checksums/t/007_pgbench_standby.pl | 14 +++-
.../test_checksums/test_checksums--1.0.sql | 8 --
.../modules/test_checksums/test_checksums.c | 79 -------------------
6 files changed, 48 insertions(+), 99 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 76004bcedc6..48326345dde 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -1197,8 +1197,15 @@ ProcessAllDatabases(void)
result = ProcessDatabase(db);
+#ifdef USE_INJECTION_POINTS
/* Allow a test process to alter the result of the operation */
- INJECTION_POINT("datachecksumsworker-modify-db-result", &result);
+ if (IS_INJECTION_POINT_ATTACHED("datachecksumsworker-fail-db-result"))
+ {
+ result = DATACHECKSUMSWORKER_FAILED;
+ INJECTION_POINT_CACHED("datachecksumsworker-fail-db-result",
+ db->dbname);
+ }
+#endif
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
++cumulative_total);
@@ -1451,6 +1458,9 @@ DataChecksumsWorkerMain(Datum arg)
BufferAccessStrategy strategy;
bool aborted = false;
int64 rels_done;
+#ifdef USE_INJECTION_POINTS
+ bool retried = false;
+#endif
operation = ENABLE_DATACHECKSUMS;
@@ -1566,7 +1576,19 @@ DataChecksumsWorkerMain(Datum arg)
}
list_free(CurrentTempTables);
- INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+#ifdef USE_INJECTION_POINTS
+ if (IS_INJECTION_POINT_ATTACHED("datachecksumsworker-fake-temptable-wait"))
+ {
+ /* Make sure to just cause one retry */
+ if (!retried && numleft == 0)
+ {
+ numleft = 1;
+ retried = true;
+
+ INJECTION_POINT_CACHED("datachecksumsworker-fake-temptable-wait", NULL);
+ }
+ }
+#endif
if (numleft == 0)
break;
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
index 897f282a1f2..ff383cf6b9d 100644
--- a/src/test/modules/test_checksums/t/005_injection.pl
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -32,6 +32,7 @@ $node->start;
# Set up test environment
$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
# ---------------------------------------------------------------------------
# Inducing failures and crashes in processing
@@ -39,9 +40,9 @@ $node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
# Force enabling checksums to fail by marking one of the databases as having
# failed in processing.
disable_data_checksums($node, wait => 1);
-$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+$node->safe_psql('postgres', "SELECT injection_points_attach('datachecksumsworker-fail-db-result','notice');");
enable_data_checksums($node, wait => 'off');
-$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+$node->safe_psql('postgres', "SELECT injection_points_detach('datachecksumsworker-fail-db-result');");
# Make sure that disabling after a failure works
disable_data_checksums($node);
@@ -66,7 +67,7 @@ SKIP:
# will force the processing to wait and retry in order to wait for it to
# disappear.
disable_data_checksums($node, wait => 1);
- $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+ $node->safe_psql('postgres', "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait', 'notice');");
enable_data_checksums($node, wait => 'on');
}
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
index 0ab5b04b931..f83b39247ae 100644
--- a/src/test/modules/test_checksums/t/006_pgbench_single.pl
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -89,14 +89,19 @@ sub background_rw_pgbench
# before and after state.
sub flip_data_checksums
{
+ my $temptablewait = 0;
+
# First, make sure the cluster is in the state we expect it to be
test_checksum_state($node, $data_checksum_state);
if ($data_checksum_state eq 'off')
{
# Coin-toss to see if we are injecting a retry due to a temptable
- $node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
- if cointoss();
+ if (cointoss())
+ {
+ $node->safe_psql('postgres', "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait', 'notice');");
+ $temptablewait = 1;
+ }
# log LSN right before we start changing checksums
my $result =
@@ -117,7 +122,8 @@ sub flip_data_checksums
random_sleep() if ($extended);
- $node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+ $node->safe_psql('postgres', "SELECT injection_points_detach('datachecksumsworker-fake-temptable-wait');")
+ if ($temptablewait);
$data_checksum_state = 'on';
}
elsif ($data_checksum_state eq 'on')
@@ -165,6 +171,7 @@ log_statement = none
]);
$node->start;
$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
# Create some content to have un-checksummed data in the cluster
$node->safe_psql('postgres',
"CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
index b0d40d24005..1cc95916dda 100644
--- a/src/test/modules/test_checksums/t/007_pgbench_standby.pl
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -99,6 +99,8 @@ sub background_pgbench
# before and after state.
sub flip_data_checksums
{
+ my $temptablewait = 0;
+
# First, make sure the cluster is in the state we expect it to be
test_checksum_state($node_primary, $data_checksum_state);
test_checksum_state($node_standby, $data_checksum_state);
@@ -106,8 +108,11 @@ sub flip_data_checksums
if ($data_checksum_state eq 'off')
{
# Coin-toss to see if we are injecting a retry due to a temptable
- $node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
- if cointoss();
+ if (cointoss())
+ {
+ $node_primary->safe_psql('postgres', "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait', 'notice');");
+ $temptablewait = 1;
+ }
# log LSN right before we start changing checksums
my $result =
@@ -153,8 +158,8 @@ sub flip_data_checksums
random_sleep() if ($extended);
wait_for_checksum_state($node_standby, 'on');
- $node_primary->safe_psql('postgres',
- 'SELECT dcw_fake_temptable(false);');
+ $node_primary->safe_psql('postgres', "SELECT injection_points_detach('datachecksumsworker-fake-temptable-wait');")
+ if ($temptablewait);
$data_checksum_state = 'on';
}
elsif ($data_checksum_state eq 'on')
@@ -207,6 +212,7 @@ log_statement = none
]);
$node_primary->start;
$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
# Create some content to have un-checksummed data in the cluster
$node_primary->safe_psql('postgres',
"CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
index 90642d247fa..c674fee02bb 100644
--- a/src/test/modules/test_checksums/test_checksums--1.0.sql
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -14,11 +14,3 @@ CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
RETURNS pg_catalog.void
AS 'MODULE_PATHNAME' LANGUAGE C;
-
-CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
- RETURNS pg_catalog.void
- AS 'MODULE_PATHNAME' LANGUAGE C;
-
-CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
- RETURNS pg_catalog.void
- AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
index e2b91f9c78c..c2eabc2821c 100644
--- a/src/test/modules/test_checksums/test_checksums.c
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -25,8 +25,6 @@ extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_d
extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
-extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
-
/*
* Test for delaying emission of procsignalbarriers.
*/
@@ -107,80 +105,3 @@ dcw_inject_startup_delay(PG_FUNCTION_ARGS)
#endif
PG_RETURN_VOID();
}
-
-#ifdef USE_INJECTION_POINTS
-static uint32 db_fail = DATACHECKSUMSWORKER_FAILED;
-#endif
-
-void
-dc_modify_db_result(const char *name, const void *private_data, void *arg)
-{
- DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
- uint32 new_res = *(uint32 *) private_data;
-
- *res = new_res;
-}
-
-PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
-Datum
-dcw_inject_fail_database(PG_FUNCTION_ARGS)
-{
-#ifdef USE_INJECTION_POINTS
- bool attach = PG_GETARG_BOOL(0);
-
- if (attach)
- InjectionPointAttach("datachecksumsworker-modify-db-result",
- "test_checksums",
- "dc_modify_db_result",
- &db_fail,
- sizeof(uint32));
- else
- InjectionPointDetach("datachecksumsworker-modify-db-result");
-#else
- elog(ERROR,
- "test is not working as intended when injection points are disabled");
-#endif
- PG_RETURN_VOID();
-}
-
-/*
- * Test to force waiting for existing temptables.
- */
-void
-dc_fake_temptable(const char *name, const void *private_data, void *arg)
-{
- static bool first_pass = true;
- int *numleft = (int *) arg;
-
- if (first_pass)
- *numleft = 1;
- first_pass = false;
-}
-
-PG_FUNCTION_INFO_V1(dcw_fake_temptable);
-Datum
-dcw_fake_temptable(PG_FUNCTION_ARGS)
-{
-#ifdef USE_INJECTION_POINTS
- bool attach = PG_GETARG_BOOL(0);
-
- if (attach)
- InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
- "test_checksums",
- "dc_fake_temptable",
- NULL,
- 0);
- else
- InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
-#else
- elog(ERROR,
- "test is not working as intended when injection points are disabled");
-#endif
- PG_RETURN_VOID();
-}
-
-void
-crash(const char *name, const void *private_data, void *arg)
-{
- abort();
-}
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0001-Wait-for-launcher-exit-in-enable-disable-checksum-te.patch (3.2K, ../../[email protected]/3-0001-Wait-for-launcher-exit-in-enable-disable-checksum-te.patch)
download | inline diff:
From eadc602e01cd871cb68f0882d066ce454f5d7c99 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Sat, 4 Apr 2026 02:12:49 +0200
Subject: [PATCH 1/2] Wait for launcher exit in enable/disable checksum tests
When enabling or disabling data checksums in a test with waiting
for an end state (on or off), the test typically want to perform
more test against the cluster immediately. Make sure to wait for
the launcher to exit in these cases before returning in order to
know it can immediately be acted on.
This is a more generic implementation of 0036232ba8f since more
buildfarm animals are showing timing related failures.
---
.../modules/test_checksums/t/001_basic.pl | 6 +----
.../test_checksums/t/DataChecksums/Utils.pm | 24 ++++++++++++++++---
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
index 5933c730da1..a78118320d5 100644
--- a/src/test/modules/test_checksums/t/001_basic.pl
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -35,11 +35,7 @@ my $result =
is($result, '9999', 'ensure checksummed pages can be read back');
# Enable data checksums again which should be a no-op so we explicitly don't
-# wait for any state transition as none should happen here. Make sure to let
-# any running launcher finish in case it's still wrapping up.
-$result = $node->poll_query_until('postgres',
- "SELECT count(*) = 0 FROM pg_catalog.pg_stat_activity WHERE backend_type = 'datachecksum launcher';"
-);
+# wait for any state transition as none should happen here.
enable_data_checksums($node);
test_checksum_state($node, 'on');
# ..and make sure we can still read/write data
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
index 9a2269e8a92..6ee3169c737 100644
--- a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -175,8 +175,19 @@ EOQ
$postgresnode->safe_psql('postgres',
sprintf($query, $params{cost_delay}, $params{cost_limit}));
- wait_for_checksum_state($postgresnode, $params{wait})
- if (defined($params{wait}));
+ if (defined($params{wait}))
+ {
+ wait_for_checksum_state($postgresnode, $params{wait});
+ # If we are tasked with waiting for an end state, also wait for the
+ # launcher to exit.
+ if ($params{wait} eq 'on' || $params{wait} eq 'off')
+ {
+ $postgresnode->poll_query_until('postgres',
+ "SELECT count(*) = 0 ".
+ "FROM pg_catalog.pg_stat_activity " .
+ "WHERE backend_type = 'datachecksum launcher';");
+ }
+ }
}
=item disable_data_checksums($node, %params)
@@ -204,7 +215,14 @@ sub disable_data_checksums
$postgresnode->safe_psql('postgres',
'SELECT pg_disable_data_checksums();');
- wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+ if (defined($params{wait}))
+ {
+ wait_for_checksum_state($postgresnode, 'off');
+ $postgresnode->poll_query_until('postgres',
+ "SELECT count(*) = 0 ".
+ "FROM pg_catalog.pg_stat_activity " .
+ "WHERE backend_type = 'datachecksum launcher';");
+ }
}
=item cointoss
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-05 04:56 Andres Freund <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Andres Freund @ 2026-04-05 04:56 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi,
On 2026-04-05 00:27:00 +0200, Daniel Gustafsson wrote:
> > On 4 Apr 2026, at 02:35, Daniel Gustafsson <[email protected]> wrote:
> >
> >> On 4 Apr 2026, at 00:59, Daniel Gustafsson <[email protected]> wrote:
> >>
> >>> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email protected]> wrote:
> >>>
> >>> After many more runs on CI I ended up pushing this version, and I see BF
> >>> members being angry due the test not waiting for the launcher to exit. I am
> >>> working on a fix right now.
> >>
> >> 0036232ba8f seems to have made the failing animals slightly happier, I will
> >> continue to monitor the buildfarm for other fallout.
> >
> > The intermittent failure on kestrel implies timing similar to the one fixed in
> > 0036232ba8fb28, a tentative fix is to make it part of waiting for an endstate
> > (on or off) to make sure the cluster is always in the right state for new
> > operations. Right now kestrel is the one which has been flapping, I'm waiting
> > a bit to see if more will follow and give further clues.
>
> mylodon had the same failure, and I believe the bug is in my injection point
> test code. I have a tentative fix in the attached refactoring which moves over
> to using the injection_point extension module. It's still fairly rare so I'm
> holding off for a little bit before pushing it to see if I can collect a little
> bit more evidence.
There are a lot checksum related errors on CI:
https://cirrus-ci.com/task/4848298592305152
https://cirrus-ci.com/task/5338691381493760
https://cirrus-ci.com/task/6271077241847808
https://cirrus-ci.com/task/6150048418889728
They probably are mostly the issues you know about. It'd be nice to get them
fixed soon-ish...
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-05 14:36 Daniel Gustafsson <[email protected]>
parent: Andres Freund <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-05 14:36 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 5 Apr 2026, at 06:56, Andres Freund <[email protected]> wrote:
>
> Hi,
>
> On 2026-04-05 00:27:00 +0200, Daniel Gustafsson wrote:
>>> On 4 Apr 2026, at 02:35, Daniel Gustafsson <[email protected]> wrote:
>>>
>>>> On 4 Apr 2026, at 00:59, Daniel Gustafsson <[email protected]> wrote:
>>>>
>>>>> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email protected]> wrote:
>>>>>
>>>>> After many more runs on CI I ended up pushing this version, and I see BF
>>>>> members being angry due the test not waiting for the launcher to exit. I am
>>>>> working on a fix right now.
>>>>
>>>> 0036232ba8f seems to have made the failing animals slightly happier, I will
>>>> continue to monitor the buildfarm for other fallout.
>>>
>>> The intermittent failure on kestrel implies timing similar to the one fixed in
>>> 0036232ba8fb28, a tentative fix is to make it part of waiting for an endstate
>>> (on or off) to make sure the cluster is always in the right state for new
>>> operations. Right now kestrel is the one which has been flapping, I'm waiting
>>> a bit to see if more will follow and give further clues.
>>
>> mylodon had the same failure, and I believe the bug is in my injection point
>> test code. I have a tentative fix in the attached refactoring which moves over
>> to using the injection_point extension module. It's still fairly rare so I'm
>> holding off for a little bit before pushing it to see if I can collect a little
>> bit more evidence.
>
> There are a lot checksum related errors on CI:
>
> https://cirrus-ci.com/task/4848298592305152
> https://cirrus-ci.com/task/5338691381493760
> https://cirrus-ci.com/task/6271077241847808
> https://cirrus-ci.com/task/6150048418889728
>
> They probably are mostly the issues you know about. It'd be nice to get them
> fixed soon-ish...
I am investigating them and have tentative fixes that I will apply tonight.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-06 00:20 Daniel Gustafsson <[email protected]>
parent: Andres Freund <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-06 00:20 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 5 Apr 2026, at 06:56, Andres Freund <[email protected]> wrote:
>
> Hi,
>
> On 2026-04-05 00:27:00 +0200, Daniel Gustafsson wrote:
>>> On 4 Apr 2026, at 02:35, Daniel Gustafsson <[email protected]> wrote:
>>>
>>>> On 4 Apr 2026, at 00:59, Daniel Gustafsson <[email protected]> wrote:
>>>>
>>>>> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email protected]> wrote:
>>>>>
>>>>> After many more runs on CI I ended up pushing this version, and I see BF
>>>>> members being angry due the test not waiting for the launcher to exit. I am
>>>>> working on a fix right now.
>>>>
>>>> 0036232ba8f seems to have made the failing animals slightly happier, I will
>>>> continue to monitor the buildfarm for other fallout.
>>>
>>> The intermittent failure on kestrel implies timing similar to the one fixed in
>>> 0036232ba8fb28, a tentative fix is to make it part of waiting for an endstate
>>> (on or off) to make sure the cluster is always in the right state for new
>>> operations. Right now kestrel is the one which has been flapping, I'm waiting
>>> a bit to see if more will follow and give further clues.
>>
>> mylodon had the same failure, and I believe the bug is in my injection point
>> test code. I have a tentative fix in the attached refactoring which moves over
>> to using the injection_point extension module. It's still fairly rare so I'm
>> holding off for a little bit before pushing it to see if I can collect a little
>> bit more evidence.
>
> There are a lot checksum related errors on CI:
>
> https://cirrus-ci.com/task/4848298592305152
[22:35:56.818] # poll_query_until timed out executing this query:
[22:35:56.818] # SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';
[22:35:56.818] # expecting this output:
[22:35:56.818] # inprogress-on
[22:35:56.818] # last actual query output:
[22:35:56.818] # on
Another timing error, solved by allowing for on as well as inprogress-on and
expanding the wait fix already committed into a more generic one.
> https://cirrus-ci.com/task/5338691381493760
This one was interesting, it managed to hit a bug when the worker process
starts, and finishes, before the launcher manages to wait for it to start up.
The BGWH_STOPPED return was erroneously interpreted as a failure.
> https://cirrus-ci.com/task/6271077241847808
Cheeky, the processing managed to finish between closing the connection
blocking progress and before shutting down the cluster. Reordering to keep the
> https://cirrus-ci.com/task/6150048418889728
Seems like the same error as the first one.
I've pushed fixes for all of these as well as the intermittent failures that
were seen on some BF animals, and will stare at the buildfarm for a while now.
So far 10 or so machines have built these green so it looks decent so far.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-06 17:00 Alexander Lakhin <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Alexander Lakhin @ 2026-04-06 17:00 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
Hello Daniel,
04.04.2026 00:46, Daniel Gustafsson wrote:
> After many more runs on CI I ended up pushing this version, and I see BF
> members being angry due the test not waiting for the launcher to exit. I am
> working on a fix right now.
Maybe this is already known or even expected, but I'd still like to let
you know that starting from f19c0ecca, I'm observing checksum errors in a
running instance. I've modified PageIsVerified() to catch errors sooner:
@@ -158,7 +158,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
if (checksum_failure)
{
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
- ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
+ ereport(PANIC,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("page verification failed, calculated checksum %u but expected %u%s",
checksum, p->pd_checksum,
And I'm getting, e.g.:
2026-04-06 18:09:12.077 EEST|postgres|regress_215|69d3cc86.3bfbdc|PANIC: page verification failed, calculated checksum
40178 but expected 50558, buffer will be zeroed
2026-04-06 18:09:12.077 EEST|postgres|regress_215|69d3cc86.3bfbdc|STATEMENT: update information_schema.sql_features set
...
Core was generated by `postgres: postgres regress_215 127.0.0.1(42448) UPDATE '.
Program terminated with signal SIGABRT, Aborted.
(gdb) bt
#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2 __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3 0x0000796d0004527e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x0000796d000288ff in __GI_abort () at ./stdlib/abort.c:79
#5 0x000055fe3f92c855 in errfinish (filename=filename@entry=0x55fe3fa54bad "bufpage.c", lineno=lineno@entry=161,
funcname=funcname@entry=0x55fe3fb70ff8 <__func__.6> "PageIsVerified") at elog.c:620
#6 0x000055fe3f7c2415 in PageIsVerified (page=page@entry=0x796cf6884000 "", blkno=blkno@entry=0, flags=10,
checksum_failure_p=checksum_failure_p@entry=0x7ffd2c524bef) at bufpage.c:161
#7 0x000055fe3f78a93d in buffer_readv_complete_one (zeroed_buffer=<synthetic pointer>, ignored_checksum=<synthetic
pointer>, failed_checksum=0x7ffd2c524bef, buffer_invalid=<synthetic pointer>, is_temp=false, failed=false, flags=9 '\t',
buffer=15424, buf_off=0 '\000', td=0x796cfc69c2d8) at bufmgr.c:8593
#8 buffer_readv_complete (is_temp=false, cb_data=<optimized out>, prior_result=..., ioh=<optimized out>) at bufmgr.c:8724
#9 shared_buffer_readv_complete (ioh=<optimized out>, prior_result=..., cb_data=<optimized out>) at bufmgr.c:8883
#10 0x000055fe3f77ec61 in pgaio_io_call_complete_shared (ioh=ioh@entry=0x796cfc69c260) at aio_callback.c:258
#11 0x000055fe3f77d4f6 in pgaio_io_process_completion (ioh=ioh@entry=0x796cfc69c260, result=<optimized out>) at aio.c:540
#12 0x000055fe3f77fe42 in pgaio_io_perform_synchronously (ioh=ioh@entry=0x796cfc69c260) at aio_io.c:146
#13 0x000055fe3f77e121 in pgaio_io_stage (ioh=ioh@entry=0x796cfc69c260, op=op@entry=PGAIO_OP_READV) at aio.c:476
#14 0x000055fe3f77fd6d in pgaio_io_start_readv (ioh=ioh@entry=0x796cfc69c260, fd=166, iovcnt=iovcnt@entry=1,
offset=offset@entry=0) at aio_io.c:87
#15 0x000055fe3f795bae in FileStartReadV (ioh=ioh@entry=0x796cfc69c260, file=<optimized out>, iovcnt=iovcnt@entry=1,
offset=offset@entry=0, wait_event_info=wait_event_info@entry=167772183) at fd.c:2225
#16 0x000055fe3f7c648b in mdstartreadv (ioh=0x796cfc69c260, reln=0x55fe73aeba98, forknum=VISIBILITYMAP_FORKNUM,
blocknum=0, buffers=<optimized out>, nblocks=1) at md.c:1041
#17 0x000055fe3f7c809c in smgrstartreadv (ioh=ioh@entry=0x796cfc69c260, reln=<optimized out>,
forknum=forknum@entry=VISIBILITYMAP_FORKNUM, blocknum=blocknum@entry=0, buffers=buffers@entry=0x7ffd2c524e70,
nblocks=nblocks@entry=1) at smgr.c:758
#18 0x000055fe3f78a1c7 in AsyncReadBuffers (operation=operation@entry=0x7ffd2c5253a0,
nblocks_progress=nblocks_progress@entry=0x7ffd2c52530c) at bufmgr.c:2144
#19 0x000055fe3f78ce19 in StartReadBuffersImpl (allow_forwarding=false, flags=9, nblocks=0x7ffd2c52530c, blockNum=0,
buffers=0x7ffd2c52539c, operation=0x7ffd2c5253a0) at bufmgr.c:1548
#20 StartReadBuffer (operation=operation@entry=0x7ffd2c5253a0, buffer=buffer@entry=0x7ffd2c52539c,
blocknum=blocknum@entry=0, flags=9) at bufmgr.c:1636
#21 0x000055fe3f78d870 in ReadBuffer_common (strategy=0x0, mode=RBM_ZERO_ON_ERROR, blockNum=0,
forkNum=VISIBILITYMAP_FORKNUM, smgr_persistence=0 '\000', smgr=0x55fe73aeba98, rel=0x796d006a31a8) at bufmgr.c:1358
#22 ReadBufferExtended (reln=reln@entry=0x796d006a31a8, forkNum=forkNum@entry=VISIBILITYMAP_FORKNUM,
blockNum=blockNum@entry=0, mode=mode@entry=RBM_ZERO_ON_ERROR, strategy=strategy@entry=0x0) at bufmgr.c:945
#23 0x000055fe3f3d7e00 in vm_readbuf (rel=rel@entry=0x796d006a31a8, blkno=blkno@entry=0, extend=extend@entry=true) at
visibilitymap.c:577
#24 0x000055fe3f3d7fda in visibilitymap_pin (rel=rel@entry=0x796d006a31a8, heapBlk=<optimized out>,
vmbuf=vmbuf@entry=0x55fe73ed2b18) at visibilitymap.c:216
#25 0x000055fe3f3d1f7a in heap_page_prune_opt (relation=0x796d006a31a8, buffer=buffer@entry=15403,
vmbuffer=vmbuffer@entry=0x55fe73ed2b18, rel_read_only=false) at pruneheap.c:339
#26 0x000055fe3f3c1dcf in heap_prepare_pagescan (sscan=sscan@entry=0x55fe73ed2a88) at heapam.c:636
#27 0x000055fe3f3c242f in heapgettup_pagemode (scan=scan@entry=0x55fe73ed2a88, dir=ForwardScanDirection, nkeys=0,
key=0x0) at heapam.c:1111
#28 0x000055fe3f3c27ab in heap_getnextslot (sscan=0x55fe73ed2a88, direction=<optimized out>, slot=0x55fe73ed13a8) at
heapam.c:1467
#29 0x000055fe3f5e8d62 in table_scan_getnextslot (sscan=<optimized out>, direction=direction@entry=ForwardScanDirection,
slot=slot@entry=0x55fe73ed13a8) at ../../../src/include/access/tableam.h:1099
#30 0x000055fe3f5e939e in SeqNext (node=0x55fe73ed1188) at nodeSeqscan.c:83
#31 ExecScanFetch (recheckMtd=0x55fe3f5e8d2e <SeqRecheck>, accessMtd=0x55fe3f5e8c9c <SeqNext>, epqstate=0x0,
node=0x55fe73ed1188) at ../../../src/include/executor/execScan.h:135
#32 ExecScanExtended (projInfo=0x55fe73ed19d8, qual=0x0, epqstate=0x0, recheckMtd=0x55fe3f5e8d2e <SeqRecheck>,
accessMtd=0x55fe3f5e8c9c <SeqNext>, node=0x55fe73ed1188) at ../../../src/include/executor/execScan.h:196
#33 ExecSeqScanWithProject (pstate=<optimized out>) at nodeSeqscan.c:164
#34 0x000055fe3f5b65f9 in ExecProcNodeFirst (node=0x55fe73ed1188) at execProcnode.c:470
#35 0x000055fe3f5dfd3b in ExecProcNode (node=node@entry=0x55fe73ed1188) at ../../../src/include/executor/executor.h:320
...
2026-04-06 18:09:12.289 EEST|postgres|regress_147|69d3cc44.3bfaaa|PANIC: page verification failed, calculated checksum
8769 but expected 0
2026-04-06 18:09:12.289 EEST|postgres|regress_147|69d3cc44.3bfaaa|STATEMENT: insert into
information_schema.sql_features values (
...
Core was generated by `postgres: postgres regress_147 127.0.0.1(35968) INSERT '.
Program terminated with signal SIGABRT, Aborted.
(gdb) bt
#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2 __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3 0x0000796d0004527e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x0000796d000288ff in __GI_abort () at ./stdlib/abort.c:79
#5 0x000055fe3f92c855 in errfinish (filename=filename@entry=0x55fe3fa54bad "bufpage.c", lineno=lineno@entry=161,
funcname=funcname@entry=0x55fe3fb70ff8 <__func__.6> "PageIsVerified") at elog.c:620
#6 0x000055fe3f7c2415 in PageIsVerified (page=page@entry=0x796cf0c80000 "", blkno=blkno@entry=21, flags=2,
checksum_failure_p=checksum_failure_p@entry=0x7ffd2c526b1f) at bufpage.c:161
#7 0x000055fe3f78a93d in buffer_readv_complete_one (zeroed_buffer=<synthetic pointer>, ignored_checksum=<synthetic
pointer>, failed_checksum=0x7ffd2c526b1f, buffer_invalid=<synthetic pointer>, is_temp=false, failed=false, flags=8 '\b',
buffer=3646, buf_off=0 '\000', td=0x796cfc63e7c8) at bufmgr.c:8593
#8 buffer_readv_complete (is_temp=false, cb_data=<optimized out>, prior_result=..., ioh=<optimized out>) at bufmgr.c:8724
#9 shared_buffer_readv_complete (ioh=<optimized out>, prior_result=..., cb_data=<optimized out>) at bufmgr.c:8883
#10 0x000055fe3f77ec61 in pgaio_io_call_complete_shared (ioh=ioh@entry=0x796cfc63e750) at aio_callback.c:258
#11 0x000055fe3f77d4f6 in pgaio_io_process_completion (ioh=ioh@entry=0x796cfc63e750, result=<optimized out>) at aio.c:540
#12 0x000055fe3f77fe42 in pgaio_io_perform_synchronously (ioh=ioh@entry=0x796cfc63e750) at aio_io.c:146
#13 0x000055fe3f77e121 in pgaio_io_stage (ioh=ioh@entry=0x796cfc63e750, op=op@entry=PGAIO_OP_READV) at aio.c:476
#14 0x000055fe3f77fd6d in pgaio_io_start_readv (ioh=ioh@entry=0x796cfc63e750, fd=199, iovcnt=iovcnt@entry=1,
offset=offset@entry=172032) at aio_io.c:87
#15 0x000055fe3f795bae in FileStartReadV (ioh=ioh@entry=0x796cfc63e750, file=<optimized out>, iovcnt=iovcnt@entry=1,
offset=offset@entry=172032, wait_event_info=wait_event_info@entry=167772183) at fd.c:2225
#16 0x000055fe3f7c648b in mdstartreadv (ioh=0x796cfc63e750, reln=0x55fe73b833b8, forknum=MAIN_FORKNUM, blocknum=21,
buffers=<optimized out>, nblocks=1) at md.c:1041
#17 0x000055fe3f7c809c in smgrstartreadv (ioh=ioh@entry=0x796cfc63e750, reln=<optimized out>,
forknum=forknum@entry=MAIN_FORKNUM, blocknum=blocknum@entry=21, buffers=buffers@entry=0x7ffd2c526da0,
nblocks=nblocks@entry=1) at smgr.c:758
#18 0x000055fe3f78a1c7 in AsyncReadBuffers (operation=operation@entry=0x7ffd2c5272d0,
nblocks_progress=nblocks_progress@entry=0x7ffd2c52723c) at bufmgr.c:2144
#19 0x000055fe3f78ce19 in StartReadBuffersImpl (allow_forwarding=false, flags=8, nblocks=0x7ffd2c52723c, blockNum=21,
buffers=0x7ffd2c5272cc, operation=0x7ffd2c5272d0) at bufmgr.c:1548
#20 StartReadBuffer (operation=operation@entry=0x7ffd2c5272d0, buffer=buffer@entry=0x7ffd2c5272cc,
blocknum=blocknum@entry=21, flags=8) at bufmgr.c:1636
#21 0x000055fe3f78d870 in ReadBuffer_common (strategy=0x0, mode=RBM_NORMAL, blockNum=21, forkNum=MAIN_FORKNUM,
smgr_persistence=0 '\000', smgr=0x55fe73b833b8, rel=0x796d0066aa18) at bufmgr.c:1358
#22 ReadBufferExtended (reln=0x796d0066aa18, forkNum=forkNum@entry=MAIN_FORKNUM, blockNum=blockNum@entry=21,
mode=mode@entry=RBM_NORMAL, strategy=strategy@entry=0x0) at bufmgr.c:945
#23 0x000055fe3f3ce074 in ReadBufferBI (relation=relation@entry=0x796d0066aa18, targetBlock=targetBlock@entry=21,
mode=mode@entry=RBM_NORMAL, bistate=bistate@entry=0x0) at hio.c:93
#24 0x000055fe3f3cea30 in RelationGetBufferForTuple (relation=relation@entry=0x796d0066aa18, len=24,
otherBuffer=otherBuffer@entry=0, options=options@entry=0, bistate=bistate@entry=0x0,
vmbuffer=vmbuffer@entry=0x7ffd2c527468, vmbuffer_other=0x0, num_pages=1) at hio.c:617
#25 0x000055fe3f3bcb50 in heap_insert (relation=relation@entry=0x796d0066aa18, tup=tup@entry=0x55fe73be9638,
cid=cid@entry=0, options=options@entry=0, bistate=bistate@entry=0x0) at heapam.c:2179
#26 0x000055fe3f3c7c82 in heapam_tuple_insert (relation=0x796d0066aa18, slot=0x55fe73be9528, cid=0, options=0,
bistate=0x0) at heapam_handler.c:267
#27 0x000055fe3f5e2da2 in table_tuple_insert (bistate=0x0, options=0, cid=<optimized out>, slot=0x55fe73be9528,
rel=0x796d0066aa18) at ../../../src/include/access/tableam.h:1456
#28 ExecInsert (context=context@entry=0x7ffd2c527620, resultRelInfo=resultRelInfo@entry=0x55fe737c8b00,
slot=0x55fe73be9528, canSetTag=true, inserted_tuple=inserted_tuple@entry=0x0, insert_destrel=insert_destrel@entry=0x0)
at nodeModifyTable.c:1272
#29 0x000055fe3f5e5542 in ExecModifyTable (pstate=0x55fe737c88f0) at nodeModifyTable.c:4933
#30 0x000055fe3f5b65f9 in ExecProcNodeFirst (node=0x55fe737c88f0) at execProcnode.c:470
...
I reproduce it rather easily (within 30 minutes) with 600 instances of
"sqlsmith --max-queries=1000" running against separate empty databases, on
my workstation with Ryzen 7900. I think I can compose a self-contained
repro, if needed... If you need more information/diagnostics, I'd be glad
to help.
Best regards,
Alexander
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-06 17:34 Daniel Gustafsson <[email protected]>
parent: Alexander Lakhin <[email protected]>
0 siblings, 0 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-06 17:34 UTC (permalink / raw)
To: Alexander Lakhin <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 6 Apr 2026, at 19:00, Alexander Lakhin <[email protected]> wrote:
>
> Hello Daniel,
>
> 04.04.2026 00:46, Daniel Gustafsson wrote:
>> After many more runs on CI I ended up pushing this version, and I see BF
>> members being angry due the test not waiting for the launcher to exit. I am
>> working on a fix right now.
>>
>
> Maybe this is already known or even expected, but I'd still like to let
> you know that starting from f19c0ecca, I'm observing checksum errors in a
> running instance.
Thanks a lot for looking, and reporting!
> I'm getting, e.g.:
> 2026-04-06 18:09:12.077 EEST|postgres|regress_215|69d3cc86.3bfbdc|PANIC: page verification failed, calculated checksum 40178 but expected 50558, buffer will be zeroed
I believe this is related to the same issue with visibilitymap clearing which
is discussed in:
https://postgr.es/m/CAAKRu_bn+e7F4yPFBgFbnP+syJRKyNK092bjD2LKvZW7O4Svag@mail.gmail.com
It was raised in this thread as well, but fixing it was deemed to be an
independent effort (and the above thread found it independently of the work in
this thread).
> I reproduce it rather easily (within 30 minutes) with 600 instances of
> "sqlsmith --max-queries=1000" running against separate empty databases, on
> my workstation with Ryzen 7900. I think I can compose a self-contained
> repro, if needed... If you need more information/diagnostics, I'd be glad
> to help.
If you have, or can easily make, a reproducer which can be boile down into a
test case then that would for sure help.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-08 07:31 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-04-08 07:31 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi Daniel,
On Fri, Apr 3, 2026 at 8:33 AM Daniel Gustafsson <[email protected]> wrote:
> The attached rebase with a PG_CONTROL_VERSION bump is what I have staged
> for
> later tonight, submitting here to have the (hopefully) final patch
> archived as
> well as another CFBot run.
>
Sorry for the late review. pg_enable/disable_data_checksums() aren't needed
to be run
on the standby. It appears the call would silently register a dynamic
background worker that
could never start , and could unexpectedly fire after promotion.
Please find the attached patch to address this.
Thanks,
Satya
Attachments:
[application/octet-stream] 0001-Prevent-pg_enable-disable_data_checksums-on-standby.patch (1.3K, ../../CAHg+QDfRk4-S7DMmdbXJnQ-xF=sUpMAKuh8b83ObLqYVKx5QLA@mail.gmail.com/3-0001-Prevent-pg_enable-disable_data_checksums-on-standby.patch)
download | inline diff:
From 128afeb5d8893b933372fda1bb865e18c9285c96 Mon Sep 17 00:00:00 2001
From: Satya Narlapuram <[email protected]>
Date: Wed, 8 Apr 2026 07:19:28 +0000
Subject: [PATCH] Prevent pg_enable/disable_data_checksums() on standby
These functions missed a RecoveryInProgress() check, allowing them to
be called on a hot standby. Adding checks to prevent them from running
on a standby.
---
src/backend/postmaster/datachecksum_state.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 1243949e..0d21e7fe 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -487,6 +487,8 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
Datum
disable_data_checksums(PG_FUNCTION_ARGS)
{
+ PreventCommandDuringRecovery("pg_disable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -507,6 +509,8 @@ enable_data_checksums(PG_FUNCTION_ARGS)
int cost_delay = PG_GETARG_INT32(0);
int cost_limit = PG_GETARG_INT32(1);
+ PreventCommandDuringRecovery("pg_enable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
--
2.43.0
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-08 20:10 Daniel Gustafsson <[email protected]>
parent: SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-08 20:10 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; PostgreSQL Hackers <[email protected]>
> On 8 Apr 2026, at 10:31, SATYANARAYANA NARLAPURAM <[email protected]> wrote:
> On Fri, Apr 3, 2026 at 8:33 AM Daniel Gustafsson <[email protected] <mailto:[email protected]>> wrote:
>> The attached rebase with a PG_CONTROL_VERSION bump is what I have staged for
>> later tonight, submitting here to have the (hopefully) final patch archived as
>> well as another CFBot run.
>
> Sorry for the late review.
No need to apologise, reviewing already committed features is one of the most
valuable contributions there is. Thank you for looking!
> pg_enable/disable_data_checksums() aren't needed to be run
> on the standby. It appears the call would silently register a dynamic background worker that
> could never start , and could unexpectedly fire after promotion.
I think this is correct, and the right fix. Since I am travelling right now I
am a bit cautious with applying code changes, so I have this one staged for
when I get back. I'll re-review, test and might add a test for it as well when
I am back in the office in Monday.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-23 10:16 Daniel Gustafsson <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-23 10:16 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>
While performing extensive post-commit testing using the built-in TAP tests in
checksum_extended mode, Tomas observed a couple issues. The testing was done
on multiple machines, ranging from a fast x86 machine to a much slower rpi4,
but they all performed a very large number of iterations. The combination of
hardware and the large number of tests executed is likely why some of the
issues went unnoticed.
Below are the issues described in more detail:
1) race condition between checkpoint and checksum state changes
The main issue is a race condition causing invalid state transitions.
When analyzing the failure (which we couldn't synthetically reproduce, only
observe by running tests over and over for a long time) we realized that the
original coding was performing checksum state transitions while replaying
online checkpoints as well as redo checkpoints.
Updating checksum state while replaying online checkpoints is incorrect. After
a crash we'll start with the REDO record, which initializes checksums to the
right state. And then later the checksum state is updated by the regular
XLOG_CHECKSUMS entries.
Moreover, the checksum state in the XLOG_CHECKPOINT_ONLINE record can be stale,
because the value is determined at the checkpoint start, but the WAL entry
is added at the end. If there is a concurrent checksum change, the value
written to the WAL record will be stale. Replaying it will cause an "invalid
transition" error later, during the next checksum state update. The fix is to
remove the checksum update from the online checkpoint altogether.
In fact, there's no need for CreateCheckPoint to update checksum state
in the ControlFile. If the value is stale, it could make it permanent.
But it's unnecessary - the ControlFile is updated by the process
performing the checksum state change. So remove that.
This, along with an re-ordering updating the controlfile and procsignal barrier
enission fixes the issue. The re-ordering makes sure that the controlfile is
always updated *before* a procsignalbarrier is emitted to avoid a race like the
one described below:
1. A barrier for off to inprogress-on is emitted
2. All active backends absorbs the barrier
- All processes in the cluster are in state inprogress-on
3. A new backend b' forks, reads controlfile and sets state of off
4. The controlfile is updated
5. A new backend b'' forks, reads controlfile and sets state to inprogress-on
b' and b'' have different states, and b' has an incompatible state with the
rest of the cluster. Re-ordering as done in the attached makes this go away.
2) race condition in launcher exit
Another timing related issue was that reverting to the "off" state then
launcher errors out had synchronization logic which was racy as it was relying
on the cached checksum state and not the value from XLogCtl. The logic for
determining if a launcher/worker was already active was also fragile as it
started another launcher which would overwrite certain data in shared memory.
The attached patch inspects shared memory instead and use that to signal the
running launcher to either abort (disable), or change cost parameters on a
running enable process. These fixes makes erroring out and going to off state
stable.
3) Concurrency issue with ProcSignalInit / InitLocalDataChecksumState
The checksum barriers must not be consumed before the initial value gets
properly set. On very slow systems, there could potentially be multiple
checksum state transitions between a fork and InitLocalDataChecksumState. In
such cases we might get failures due to incorrect transitions.
With the current code this is not a live issue, as there is no place checking
interrupts in between the two functions. But it's fragile, as it's trivial to
break this by adding an elog() somewhere. Which is what happened to us while
debugging the other issues. So better to explicitly hold interrupts for a
brief moment.
To find the issues, and to validate their fix, Tomas developed a new testsuite
which is attached as a .txt. This is not proposed for adding to v19, it is
included to showcase what was done, and what will be further hacked on for a
new suite during the v20 cycle. It is gated behind PG_TEST_EXTRA and is
intended to be executed by select members of the buildfarm.
As part of this postcommit review we also found a few more cleanups and smaller
fixes which are included. The patchset also includes the patch submitted
upthread by Satyanarayana Narlapuram.
More details can be found in the individual commit messages.
Unless there are objections I would like to go ahead with this fixup fairly soon.
--
Daniel Gustafsson
commit f502fa990e877c6e4d1d03518e524519d8e88806
Author: Tomas Vondra <[email protected]>
Date: Wed Apr 15 12:50:06 2026 +0200
POC: Test checksum state transitions using step through injection points
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index be92b6af20f..5b481784543 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4762,6 +4762,9 @@ SetDataChecksumsOnInProgress(void)
uint32 data_checksum_version;
elog(LOG, "SetDataChecksumsOnInProgress / start");
+
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-delay", NULL);
+
/*
* The state transition is performed in a critical section with
* checkpoints held off to provide crash safety.
@@ -4782,6 +4785,8 @@ SetDataChecksumsOnInProgress(void)
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version %u => %u",
@@ -4791,6 +4796,8 @@ SetDataChecksumsOnInProgress(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOnInProgress / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON);
@@ -4862,6 +4869,8 @@ SetDataChecksumsOn(void)
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-enable-checksums-after-xlogctl", NULL);
+
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
@@ -4875,9 +4884,13 @@ SetDataChecksumsOn(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-enable-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-enable-checksums-after-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOn / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION);
@@ -4924,6 +4937,8 @@ SetDataChecksumsOff(void)
{
SpinLockRelease(&XLogCtl->info_lck);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-delay", NULL);
+
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
@@ -4934,24 +4949,30 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
- elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u = %u",
+ elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
- elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u = %u",
+ elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOff / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF);
@@ -4971,6 +4992,8 @@ SetDataChecksumsOff(void)
SpinLockRelease(&XLogCtl->info_lck);
}
+ INJECTION_POINT("datachecksums-disable-checksums-delay", NULL);
+
START_CRIT_SECTION();
/* Ensure that we don't incur a checkpoint during disabling checksums */
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
@@ -4988,6 +5011,8 @@ SetDataChecksumsOff(void)
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-disable-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -4997,9 +5022,13 @@ SetDataChecksumsOff(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-disable-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-disable-checksums-after-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOff / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF);
diff --git a/src/test/modules/test_checksums/t/010_injection_2.pl b/src/test/modules/test_checksums/t/010_injection_2.pl
new file mode 100644
index 00000000000..a8ae9ffd151
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_injection_2.pl
@@ -0,0 +1,209 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition. The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - first - first checksum change
+# - second - second checksum change
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers two
+# checksum changes, and verifies the final state is as expected. The first
+# state change is paused on a selected injection point, and unpaused after
+# the second change gets initiated.
+#
+# The injection point is triggered only by the datachecksum launcher, and
+# there can be only one such process. So there's no risk of hitting the
+# injection point by both changes.
+sub test_checksum_transition
+{
+ my ($start, $first, $second, $point, $final) = @_;
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $first . " / " . $second . " / " . $point . " / " . $final . " =========='");
+
+ note($start . " / " . $first . " / " . $second . " / " . $point . " / " . $final);
+
+ note('changing checksums into initial state: ' . $start);
+
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+
+ note("triggering first checksum change: " . $first);
+
+ enable_data_checksums($node) if ($first eq 'enable');
+ disable_data_checksums($node) if ($first eq 'disable');
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+
+ note("triggering second checksum change: " . $second);
+
+ enable_data_checksums($node) if ($second eq 'enable');
+ disable_data_checksums($node) if ($second eq 'disable');
+
+ note("waking and detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');");
+
+ note("detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-checkpoint', 'off');
+
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-delay', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-delay', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-checkpoint', 'on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/011_injection_checkpoint.pl b/src/test/modules/test_checksums/t/011_injection_checkpoint.pl
new file mode 100644
index 00000000000..ce1553e8ecd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/011_injection_checkpoint.pl
@@ -0,0 +1,196 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition concurrent with a checkpoint.
+#
+# The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers a
+# checksum change concurrent with a checkpoint, and verifies the final state
+# is as expected. The state change is paused on a selected injection
+# point, and unpaused after performing a checkpoint.
+#
+# Finally, the instance is restarted (in either fast ot immediate mode),
+# the final checksum state is validated against the expected value, and
+# the server log is checked for checksum failures.
+sub test_checksum_transition
+{
+ my ($start, $change, $point, $final) = @_;
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / " . $point . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / " . $point . " / " . $final);
+
+ note('changing checksums into initial state: ' . $start);
+
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+
+ note("triggering checksum change: " . $change);
+
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+
+ note('checkpoint');
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ note("waking and detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');");
+
+ note("detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+
+ test_checksum_state($node, $final);
+
+ $node->stop(stopmode());
+ $node->start;
+
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl
new file mode 100644
index 00000000000..98e9c805c41
--- /dev/null
+++ b/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl
@@ -0,0 +1,214 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition concurrent with a checkpoint.
+#
+# The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point1 - injection point before checkpoint
+# - point2 - injection point after checkpoint
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers a
+# checksum change that pauses on a selected injection point. Then performs
+# a checkpoint, unpauses the change so that it proceeds to a second
+# injection point.
+#
+# Then the instance is restarted in immediate mode to simulate failure,
+# and the final checksum state is validated against the expected value.
+# The server log is checked for checksum failures.
+sub test_checksum_transition
+{
+ my ($start, $change, $point1, $point2, $final) = @_;
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / " . $point1 . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / " . $point1 . " / " . $final);
+
+ note('changing checksums into initial state: ' . $start);
+
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ note('attaching injection point: ' . $point1);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point1 . "','wait');"
+ );
+
+ if (defined($point2))
+ {
+ note('attaching injection point: ' . $point2);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point2 . "','wait');"
+ );
+ }
+
+ note("triggering checksum change: " . $change);
+
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point1 . "'",
+ '1');
+
+ note('checkpoint');
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ note("waking and detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point1 . "');");
+
+ note("detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point1 . "');");
+
+ if (defined($point2))
+ {
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point2 . "'",
+ '1');
+ }
+ else
+ {
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+ }
+
+ $node->stop('immediate');
+ $node->start;
+
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-delay', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-checkpoint', undef, 'on');
+
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-delay', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-checkpoint', undef, 'off');
+
+$node->stop;
+done_testing();
Attachments:
[application/octet-stream] 0008-Fix-data_checksum-GUC-show_hook.patch (971B, ../../[email protected]/2-0008-Fix-data_checksum-GUC-show_hook.patch)
download | inline diff:
From 0a2b961f90427937796b5bca549b04e20958cd0f Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:47 +0200
Subject: [PATCH 8/8] Fix data_checksum GUC show_hook
Commit f19c0eccae erroneously omitted the show_hook for the
data_checksum GUC.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/utils/misc/guc_parameters.dat | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 83af594d4af..afaa058b046 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -585,6 +585,7 @@
variable => 'data_checksums',
boot_val => 'PG_DATA_CHECKSUM_OFF',
options => 'data_checksums_options',
+ show_hook => 'show_data_checksums',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0007-Improve-database-detection-logic-in-datachecksumswor.patch (2.3K, ../../[email protected]/3-0007-Improve-database-detection-logic-in-datachecksumswor.patch)
download | inline diff:
From 3e0fbc07bade81a423b668d975111bb02067442f Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:40 +0200
Subject: [PATCH 7/8] Improve database detection logic in datachecksumsworker
The worker need to know whether a database which failed checksum
processing still exists, or has been dropped. This improves the
detection logic by checking for being partially dropped.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/postmaster/datachecksum_state.c | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index c382b954d91..53b66cbbcf6 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -843,8 +843,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
/*
* Heuristic to see if the database was dropped, and if it was we can
- * treat it as not an error, else treat as fatal and error out. TODO:
- * this could probably be improved with a tighter check.
+ * treat it as not an error, else treat as fatal and error out.
*/
if (DatabaseExists(db->dboid))
return DATACHECKSUMSWORKER_FAILED;
@@ -1305,7 +1304,9 @@ DataChecksumsShmemRequest(void *arg)
* DatabaseExists
*
* Scans the system catalog to check if a database with the given Oid exists
- * and returns true if it is found, else false.
+ * and returns true if it is found and valid, else false. Note, we cannot use
+ * database_is_invalid_oid here as it will ERROR out, and we want to gracefully
+ * handle errors.
*/
static bool
DatabaseExists(Oid dboid)
@@ -1315,6 +1316,7 @@ DatabaseExists(Oid dboid)
SysScanDesc scan;
bool found;
HeapTuple tuple;
+ Form_pg_database pg_database_tuple;
StartTransactionCommand();
@@ -1328,6 +1330,14 @@ DatabaseExists(Oid dboid)
tuple = systable_getnext(scan);
found = HeapTupleIsValid(tuple);
+ /* If the Oid exists, ensure that it's not partially dropped */
+ if (found)
+ {
+ pg_database_tuple = (Form_pg_database) GETSTRUCT(tuple);
+ if (database_is_invalid_form(pg_database_tuple))
+ found = false;
+ }
+
systable_endscan(scan);
table_close(rel, AccessShareLock);
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0006-Improve-handling-of-concurrent-checksum-requests.patch (9.8K, ../../[email protected]/4-0006-Improve-handling-of-concurrent-checksum-requests.patch)
download | inline diff:
From 7517ce4a6c33be0f75780ade699afc1e807cf997 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:32 +0200
Subject: [PATCH 6/8] Improve handling of concurrent checksum requests
When pg_{enable|disable}_data_checksums is called while checksums are
being enabled of disabled, the already running launcher is detected
and the new desired state is recorded. Processing will then pick up
the new state and change it's operation to fulfill the new request.
If the same state is requested but updated cost values, the new cost
values will take effect on the next relation processed. The previous
coding had a complex logic of starting a new launcher for this, which
ss now avoided with the shared mem structure instead used to signal
current processing.
This makes the logic more robust, and fixes a bug where the launcher
would erroneously revert back to the "off" state.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 33 +++++++-
src/backend/postmaster/datachecksum_state.c | 85 +++++++++++++++------
src/include/access/xlog.h | 2 +
3 files changed, 96 insertions(+), 24 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9b1a10dbd1c..49772510855 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4682,10 +4682,41 @@ DataChecksumsNeedWrite(void)
LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
}
+
+bool
+DataChecksumsOff(void)
+{
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_OFF);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
+}
+
+bool
+DataChecksumsOn(void)
+{
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
+}
+
bool
DataChecksumsInProgressOn(void)
{
- return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
}
/*
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 5e14803b0b0..c382b954d91 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -235,7 +235,7 @@ typedef struct ChecksumBarrierCondition
int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[7] =
+static const ChecksumBarrierCondition checksum_barriers[9] =
{
/*
* Disabling checksums: If checksums are currently enabled, disabling must
@@ -267,6 +267,13 @@ static const ChecksumBarrierCondition checksum_barriers[7] =
* set to off since we cannot reach on at that point.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+
+ /*
+ * Transitions that can happen when a new request is made while another is
+ * currently being processed.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
/*
@@ -368,6 +375,15 @@ const ShmemCallbacks DataChecksumsShmemCallbacks = {
.request_fn = DataChecksumsShmemRequest,
};
+#define CHECK_FOR_ABORT_REQUEST() \
+ do { \
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); \
+ if (DataChecksumState->launch_operation != operation) \
+ abort_requested = true; \
+ LWLockRelease(DataChecksumsWorkerLock); \
+ } while (0)
+
+
/*****************************************************************************
* Functionality for manipulating the data checksum state in the cluster
*/
@@ -557,7 +573,6 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
BackgroundWorker bgw;
BackgroundWorkerHandle *bgw_handle;
bool running;
- DataChecksumsWorkerOperation launcher_running_op;
#ifdef USE_ASSERT_CHECKING
/* The cost delay settings have no effect when disabling */
@@ -576,8 +591,6 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
/* Is the launcher already running? If so, what is it doing? */
running = DataChecksumState->launcher_running;
- if (running)
- launcher_running_op = DataChecksumState->operation;
LWLockRelease(DataChecksumsWorkerLock);
@@ -594,13 +607,17 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
* the launcher has had a chance to start up, we still end up launching it
* twice. That's OK, the second invocation will see that a launcher is
* already running and exit quickly.
- *
- * TODO: We could optimize here and skip launching the launcher, if we are
- * already in the desired state, i.e. if the checksums are already enabled
- * and you call pg_enable_data_checksums().
*/
if (!running)
{
+ if ((op == ENABLE_DATACHECKSUMS && DataChecksumsOn()) ||
+ (op == DISABLE_DATACHECKSUMS && DataChecksumsOff()))
+ {
+ ereport(LOG,
+ errmsg("data checksums already in desired state, exiting"));
+ return;
+ }
+
/*
* Prepare the BackgroundWorker and launch it.
*/
@@ -622,9 +639,8 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
}
else
{
- if (launcher_running_op == op)
- ereport(ERROR,
- errmsg("data checksum processing already running"));
+ ereport(LOG,
+ errmsg("data checksum processing already running"));
}
}
@@ -998,11 +1014,8 @@ WaitForAllTransactionsToFinish(void)
errhint("Data checksums processing must be restarted manually after cluster restart."));
CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
- LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumState->launch_operation != operation)
- abort_requested = true;
- LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
break;
}
@@ -1185,7 +1198,9 @@ ProcessAllDatabases(void)
int cumulative_total = 0;
/* Set up so first run processes shared catalogs, not once in every db */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->process_shared_catalogs = true;
+ LWLockRelease(DataChecksumsWorkerLock);
/* Get a list of all databases to process */
WaitForAllTransactionsToFinish();
@@ -1261,7 +1276,9 @@ ProcessAllDatabases(void)
* When one database has completed, it will have done shared catalogs
* so we don't have to process them again.
*/
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->process_shared_catalogs = false;
+ LWLockRelease(DataChecksumsWorkerLock);
}
FreeDatabaseList(DatabaseList);
@@ -1503,7 +1520,6 @@ DataChecksumsWorkerMain(Datum arg)
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
*/
- Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
VacuumCostDelay = DataChecksumState->cost_delay;
VacuumCostLimit = DataChecksumState->cost_limit;
VacuumCostActive = (VacuumCostDelay > 0);
@@ -1539,8 +1555,6 @@ DataChecksumsWorkerMain(Datum arg)
rels_done = 0;
foreach_oid(reloid, RelationList)
{
- CHECK_FOR_INTERRUPTS();
-
if (!ProcessSingleRelationByOid(reloid, strategy))
{
aborted = true;
@@ -1549,12 +1563,38 @@ DataChecksumsWorkerMain(Datum arg)
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
++rels_done);
+ CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
+
+ if (abort_requested)
+ break;
+
+ /* Check if the cost settings changed during runtime */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
+ || (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
+ {
+ VacuumCostDelay = DataChecksumState->launch_cost_delay;
+ VacuumCostLimit = DataChecksumState->launch_cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+
+ FreeAccessStrategy(strategy);
+ strategy = GetAccessStrategy(BAS_VACUUM);
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+
}
+
list_free(RelationList);
+ FreeAccessStrategy(strategy);
- if (aborted)
+ if (aborted || abort_requested)
{
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ LWLockRelease(DataChecksumsWorkerLock);
ereport(DEBUG1,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1619,15 +1659,14 @@ DataChecksumsWorkerMain(Datum arg)
3000,
WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
- LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- aborted = DataChecksumState->launch_operation != operation;
- LWLockRelease(DataChecksumsWorkerLock);
-
CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
if (aborted || abort_requested)
{
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ LWLockRelease(DataChecksumsWorkerLock);
ereport(LOG,
errmsg("data checksum processing aborted in database OID %u",
dboid));
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 437b4f32349..4dd98624204 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -249,6 +249,8 @@ extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
extern bool DataChecksumsNeedWrite(void);
extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsOn(void);
+extern bool DataChecksumsOff(void);
extern bool DataChecksumsInProgressOn(void);
extern void SetDataChecksumsOnInProgress(void);
extern void SetDataChecksumsOn(void);
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0005-Typo-and-spelling-fixups-for-online-checksums.patch (2.6K, ../../[email protected]/5-0005-Typo-and-spelling-fixups-for-online-checksums.patch)
download | inline diff:
From ae0f05e8134cde61895b37918f0e968ae0ef1e32 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:28 +0200
Subject: [PATCH 5/8] Typo and spelling fixups for online checksums
A collection of spelling, wording and punctuation fixups for the code
documentation from postcommit review.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/postmaster/datachecksum_state.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 6d66322c625..5e14803b0b0 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -86,7 +86,7 @@
* failing to validate the data checksum on the page when reading it.
*
* When processing starts all backends belong to one of the below sets, with
- * one if Bd and Bi being empty:
+ * one of Bd and Bi being empty:
*
* Bg: Backend updating the global state and emitting the procsignalbarrier
* Bd: Backends in "off" state
@@ -286,7 +286,7 @@ typedef struct DataChecksumsStateStruct
int launch_cost_limit;
/*
- * Is a launcher process is currently running? This is set by the main
+ * Is a launcher process currently running? This is set by the main
* launcher process, after it has read the above launch_* parameters.
*/
bool launcher_running;
@@ -809,7 +809,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
{
/*
* If the worker managed to start, and stop, before we got to waiting
- * for it we can se a STOPPED status here without it being a failure.
+ * for it we can see a STOPPED status here without it being a failure.
*/
if (DataChecksumState->success == DATACHECKSUMSWORKER_SUCCESSFUL)
{
@@ -1288,7 +1288,7 @@ DataChecksumsShmemRequest(void *arg)
* DatabaseExists
*
* Scans the system catalog to check if a database with the given Oid exists
- * and returns true if it is found else false.
+ * and returns true if it is found, else false.
*/
static bool
DatabaseExists(Oid dboid)
@@ -1451,7 +1451,7 @@ BuildRelationList(bool temp_relations, bool include_shared)
/*
* DataChecksumsWorkerMain
*
- * Main function for enabling checksums in a single database, This is the
+ * Main function for enabling checksums in a single database. This is the
* function set as the bgw_function_name in the dynamic background worker
* process initiated for each database by the worker launcher. After enabling
* data checksums in each applicable relation in the database, it will wait for
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0004-Fix-invalid-checksum-state-transition-in-checkpoints.patch (13.1K, ../../[email protected]/6-0004-Fix-invalid-checksum-state-transition-in-checkpoints.patch)
download | inline diff:
From cf5de7abe443acc311f4880499be520f00a7bf5e Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:22 +0200
Subject: [PATCH 4/8] Fix invalid checksum state transition in checkpoints
Commit 78e950cb8 added checksum state handling to all XLOG_CHECKPOINT
records which caused unnecessary state transitions and emission of
procsignal barriers. Remove as only the _REDO record need to handle
checksum state. Barrier emission is also consistently made after
controlfile updates to avoid race conditions.
Additionelly, interrupts are held between calling ProcSignalInit and
InitLocalDataChecksumState to remove a window where otherwise invalid
state transitions can happen.
Also remove a pointless assertion on Controlfile which will never hit.
Author: Tomas Vondra <[email protected]>
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 106 ++++++--------------
src/backend/postmaster/auxprocess.c | 10 ++
src/backend/postmaster/datachecksum_state.c | 8 +-
src/backend/utils/init/postinit.c | 10 ++
4 files changed, 55 insertions(+), 79 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f74d7a2ab1a..9b1a10dbd1c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4723,8 +4723,6 @@ SetDataChecksumsOnInProgress(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
/*
* The state transition is performed in a critical section with
* checkpoints held off to provide crash safety.
@@ -4738,25 +4736,16 @@ SetDataChecksumsOnInProgress(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
- /*
- * Update the controlfile before waiting since if we have an immediate
- * shutdown while waiting we want to come back up with checksums enabled.
- */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
UpdateControlFile();
LWLockRelease(ControlFileLock);
- /*
- * Await state change in all backends to ensure that all backends are in
- * "inprogress-on". Once done we know that all backends are writing data
- * checksums.
- */
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
WaitForProcSignalBarrier(barrier);
}
@@ -4787,8 +4776,6 @@ SetDataChecksumsOn(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
SpinLockAcquire(&XLogCtl->info_lck);
/*
@@ -4818,11 +4805,6 @@ SetDataChecksumsOn(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
@@ -4832,12 +4814,12 @@ SetDataChecksumsOn(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
- RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
- /*
- * Await state transition to "on" in all backends. When done we know that
- * data checksums are both written and verified in all backends.
- */
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
WaitForProcSignalBarrier(barrier);
}
@@ -4859,8 +4841,6 @@ SetDataChecksumsOff(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
SpinLockAcquire(&XLogCtl->info_lck);
/* If data checksums are already disabled there is nothing to do */
@@ -4891,22 +4871,17 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
- LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
- ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
- UpdateControlFile();
- LWLockRelease(ControlFileLock);
-
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
-
- /*
- * Update local state in all backends to ensure that any backend in
- * "on" state is changed to "inprogress-off".
- */
WaitForProcSignalBarrier(barrier);
/*
@@ -4935,18 +4910,17 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
- RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
WaitForProcSignalBarrier(barrier);
}
@@ -4961,6 +4935,7 @@ SetDataChecksumsOff(void)
void
InitLocalDataChecksumState(void)
{
+ Assert(InterruptHoldoffCount > 0);
SpinLockAcquire(&XLogCtl->info_lck);
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
@@ -5427,7 +5402,6 @@ XLOGShmemInit(void *arg)
/* Use the checksum info from control file */
XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
-
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
@@ -7518,7 +7492,9 @@ CreateCheckPoint(int flags)
* Get the current data_checksum_version value from xlogctl, valid at the
* time of the checkpoint.
*/
+ SpinLockAcquire(&XLogCtl->info_lck);
checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
if (shutdown)
{
@@ -7639,10 +7615,6 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
- SpinLockAcquire(&XLogCtl->info_lck);
- checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
- SpinLockRelease(&XLogCtl->info_lck);
-
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7792,9 +7764,6 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
- /* make sure we start with the checksum version as of the checkpoint */
- ControlFile->data_checksum_version = checkPoint.dataChecksumState;
-
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -8871,11 +8840,6 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
- SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
- SetLocalDataChecksumState(checkPoint.dataChecksumState);
- SpinLockRelease(&XLogCtl->info_lck);
-
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8936,6 +8900,8 @@ xlog_redo(XLogReaderState *record)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
+ UpdateControlFile();
LWLockRelease(ControlFileLock);
/*
@@ -8962,8 +8928,6 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
- bool new_state = false;
- int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -9002,8 +8966,6 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
- old_state = ControlFile->data_checksum_version;
- ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -9015,18 +8977,6 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
- /*
- * If the data checksum state change we need to emit a barrier.
- */
- SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
- if (checkPoint.dataChecksumState != old_state)
- new_state = true;
- SpinLockRelease(&XLogCtl->info_lck);
-
- if (new_state)
- EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
-
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -9195,6 +9145,7 @@ xlog_redo(XLogReaderState *record)
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ SetLocalDataChecksumState(redo_rec.data_checksum_version);
if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
new_state = true;
SpinLockRelease(&XLogCtl->info_lck);
@@ -9268,6 +9219,11 @@ xlog2_redo(XLogReaderState *record)
XLogCtl->data_checksum_version = state.new_checksum_state;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = state.new_checksum_state;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
/*
* Block on a procsignalbarrier to await all processes having seen the
* change to checksum status. Once the barrier has been passed we can
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 8fdc518b3a1..421502fa1cb 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -68,6 +68,14 @@ AuxiliaryProcessMainCommon(void)
BaseInit();
+ /*
+ * Prevent consuming interrups between setting ProcSignalInit and setting
+ * the initial local data checksum value. If a barrier is emitted, and
+ * absorbed, before local cached state is initialized the state transition
+ * can be invalid.
+ */
+ HOLD_INTERRUPTS();
+
ProcSignalInit(NULL, 0);
/*
@@ -88,6 +96,8 @@ AuxiliaryProcessMainCommon(void)
*/
InitLocalDataChecksumState();
+ RESUME_INTERRUPTS();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index e62a36b0af5..6d66322c625 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -263,8 +263,8 @@ static const ChecksumBarrierCondition checksum_barriers[7] =
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
/*
- * If checksums are being enabled when launcher_exit is executed, state
- * is set to off since we cannot reach on at that point.
+ * If checksums are being enabled when launcher_exit is executed, state is
+ * set to off since we cannot reach on at that point.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
@@ -1287,8 +1287,8 @@ DataChecksumsShmemRequest(void *arg)
/*
* DatabaseExists
*
- * Scans the system catalog to check if a database with the given Oid exist
- * and returns true if it is found, else false.
+ * Scans the system catalog to check if a database with the given Oid exists
+ * and returns true if it is found else false.
*/
static bool
DatabaseExists(Oid dboid)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 6f074013aa9..b2c80b0bd1e 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -756,6 +756,14 @@ InitPostgres(const char *in_dbname, Oid dboid,
*/
SharedInvalBackendInit(false);
+ /*
+ * Prevent consuming interrups between setting ProcSignalInit and setting
+ * the initial local data checksum value. If a barrier is emitted, and
+ * absorbed, before local cached state is initialized the state transition
+ * can be invalid.
+ */
+ HOLD_INTERRUPTS();
+
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
/*
@@ -776,6 +784,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
*/
InitLocalDataChecksumState();
+ RESUME_INTERRUPTS();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0003-Handle-data_checksum-state-changes-during-launcher_e.patch (3.2K, ../../[email protected]/7-0003-Handle-data_checksum-state-changes-during-launcher_e.patch)
download | inline diff:
From b24179efcd960c15a1fa2309bb5107c5625e33e8 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:16 +0200
Subject: [PATCH 3/8] Handle data_checksum state changes during launcher_exit
When erroring out from the datachecksums launcher during data checksum
enabling, before state has transitioned to "on", we revert back to the
"off" state. Since checksums weren't enabled, there is no use staying
in an inprogress state since the checksum launcher currently doesn't
support restarting from where it left off. Should restartability get
added in the future, this would need to be revisited. This state
transition was however missing from the allowed transitions in the
statemachine causing an error.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 15 ++++++++-------
src/backend/postmaster/datachecksum_state.c | 8 +++++++-
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e39af79c03b..f74d7a2ab1a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4871,13 +4871,14 @@ SetDataChecksumsOff(void)
}
/*
- * If data checksums are currently enabled we first transition to the
- * "inprogress-off" state during which backends continue to write
- * checksums without verifying them. When all backends are in
- * "inprogress-off" the next transition to "off" can be performed, after
- * which all data checksum processing is disabled.
- */
- if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ * If data checksums are currently enabled, or in the process of being
+ * enabled, we first transition to the "inprogress-off" state during which
+ * backends continue to write checksums without verifying them. When all
+ * backends are in "inprogress-off" the next transition to "off" can be
+ * performed, after which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION ||
+ XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
{
SpinLockRelease(&XLogCtl->info_lck);
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index ea102086144..e62a36b0af5 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -235,7 +235,7 @@ typedef struct ChecksumBarrierCondition
int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[6] =
+static const ChecksumBarrierCondition checksum_barriers[7] =
{
/*
* Disabling checksums: If checksums are currently enabled, disabling must
@@ -261,6 +261,12 @@ static const ChecksumBarrierCondition checksum_barriers[6] =
* checksums, we can go straight back to 'on'
*/
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being enabled when launcher_exit is executed, state
+ * is set to off since we cannot reach on at that point.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
/*
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0002-Test-improvements-for-online-checksums.patch (7.3K, ../../[email protected]/8-0002-Test-improvements-for-online-checksums.patch)
download | inline diff:
From a24b6fc17b633514de2ca5064c65029910be06ac Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:44:09 +0200
Subject: [PATCH 2/8] Test improvements for online checksums
This includes a number of smaller fixups to the onine checksums test
module which were found during postcommit review and stabilization
work.
* Fix scope increase for PG_TEST_EXTRA: The online checksums tests
have two levels of PG_TEST_EXTRA, checksum and checksums_extended
for extra test runs and test runs with increased randomization.
The logic for increasing the number of test iterations was however
backwards.
* Change stopmode for PITR test: The pitr suite used immediate stop
mode which caused problems on slower machines where the sigquit
would interrupt archive commands leaving partial WAL files behind.
This would then prevent restart. Fix by using fast mode which is
the appropriate mode for the test at hand. Also increase timeouts
to help slower test systems since an expired timeout will incur
the same effect as an immediate standby with a partial WAL left
behind. This issue was observed when running the test suites on
a Raspberry Pi 4 machine.
* Improve logging: The test suite for data checksums use a set of
helper functions in a Perl module to avoid repeating code, this
makes sure that the helper functions do a better job of logging
their test output to make debug easier.
* Remove unused code: wait_for_cluster_crash was used during the
development of online checksums but was never used in any test
which shipped, so remove the function.
* Standby fixes: Ensure no vacuum on pgbench init on standby with
-n to avoid bogus error message in the log, and enable
hot_standby_feedback to prevent queries from getting cancelled
due to recovery on slower systems.
Author: Daniel Gustafsson <[email protected]>
Author: Tomas Vondra <[email protected]>
Discussion: https://postgr.es/m/xxx
---
.../test_checksums/t/007_pgbench_standby.pl | 12 +++--
src/test/modules/test_checksums/t/008_pitr.pl | 5 +-
.../test_checksums/t/DataChecksums/Utils.pm | 53 +++----------------
3 files changed, 20 insertions(+), 50 deletions(-)
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
index f3611e7ce25..0b3996f1d69 100644
--- a/src/test/modules/test_checksums/t/007_pgbench_standby.pl
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -49,8 +49,8 @@ my $node_standby_loglocation = 0;
# of tests performed and the wall time taken is non-deterministic as the test
# performs a lot of randomized actions, but 5 iterations will be a long test
# run regardless.
-my $TEST_ITERATIONS = 5;
-$TEST_ITERATIONS = 1 if ($extended);
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 5 if ($extended);
# Variables which record the current state of the cluster
my $data_checksum_state = 'off';
@@ -83,6 +83,7 @@ sub background_pgbench
push(@cmd, '-C') if ($extended && cointoss());
# If we run on a standby it needs to be a read-only benchmark
push(@cmd, '-S') if ($standby);
+ push(@cmd, '-n') if ($standby);
# Finally add the database name to use
push(@cmd, 'postgres');
@@ -146,8 +147,10 @@ sub flip_data_checksums
. "FROM pg_catalog.pg_settings "
. "WHERE name = 'data_checksums';");
- is(($result eq 'inprogress-on' || $result eq 'on'),
- 1, 'ensure checksums are on, or in progress, on standby_1');
+ is( ($result eq 'inprogress-on' || $result eq 'on'),
+ 1,
+ 'ensure checksums are on, or in progress, on standby_1, got: '
+ . $result);
# Wait for checksums enabled on the primary and standby
wait_for_checksum_state($node_primary, 'on');
@@ -210,6 +213,7 @@ $node_primary->append_conf(
qq[
max_connections = 30
log_statement = none
+hot_standby_feedback = on
]);
$node_primary->start;
$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
index e8cb2b0ed96..1f8176686fd 100644
--- a/src/test/modules/test_checksums/t/008_pitr.pl
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -124,11 +124,14 @@ $node_primary->init(
has_archiving => 1,
allows_streaming => 1,
no_data_checksums => 1);
+my $timeout_unit = 's';
$node_primary->append_conf(
'postgresql.conf',
qq[
max_connections = 100
log_statement = none
+wal_sender_timeout = $PostgreSQL::Test::Utils::timeout_default$timeout_unit
+wal_receiver_timeout = $PostgreSQL::Test::Utils::timeout_default$timeout_unit
]);
$node_primary->start;
@@ -154,7 +157,7 @@ my ($pre_lsn, $post_lsn) = flip_data_checksums();
$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
-$node_primary->stop('immediate');
+$node_primary->stop('fast');
my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
$node_pitr->init_from_backup(
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
index fb704623a60..cb78dd6ecfb 100644
--- a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -43,7 +43,6 @@ our @EXPORT = qw(
stopmode
test_checksum_state
wait_for_checksum_state
- wait_for_cluster_crash
);
=pod
@@ -67,7 +66,10 @@ sub test_checksum_state
my $result = $postgresnode->safe_psql('postgres',
"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
);
- is($result, $state, 'ensure checksums are set to ' . $state);
+ is($result, $state,
+ 'ensure checksums are set to '
+ . $state . ' on '
+ . $postgresnode->name());
return $result eq $state;
}
@@ -89,52 +91,13 @@ sub wait_for_checksum_state
'postgres',
"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
$state);
- is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ is($res, 1,
+ 'ensure data checksums are transitioned to '
+ . $state . ' on '
+ . $postgresnode->name());
return $res == 1;
}
-=item wait_for_cluster_crash(node, params)
-
-Repeatedly test if the cluster running at B<node> responds to connections
-and return when it no longer does so, or when it times out. Processing will
-run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
-value is specified as a parameter. Returns True if the cluster crashed, else
-False if the process timed out.
-
-=over
-
-=item timeout
-
-Approximate number of seconds to wait for cluster to crash, default is
-$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
-that the total process time won't exceed the timeout.
-
-=back
-
-=cut
-
-sub wait_for_cluster_crash
-{
- my $postgresnode = shift;
- my %params = @_;
- my $crash = 0;
-
- $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
- unless (defined($params{timeout}));
-
- for (my $naps = 0; $naps < $params{timeout}; $naps++)
- {
- if (!$postgresnode->is_alive)
- {
- $crash = 1;
- last;
- }
- sleep(1);
- }
-
- return $crash == 1;
-}
-
=item enable_data_checksums($node, %params)
Function for enabling data checksums in the cluster running at B<node>.
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0001-Prevent-pg_enable-disable_data_checksums-on-standby.patch (1.5K, ../../[email protected]/9-0001-Prevent-pg_enable-disable_data_checksums-on-standby.patch)
download | inline diff:
From 96f0fc24add90a7c0af0e3700c24b835f4497a7b Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 23 Apr 2026 10:43:58 +0200
Subject: [PATCH 1/8] Prevent pg_enable/disable_data_checksums() on standby
These functions missed a RecoveryInProgress() check, allowing them to
be called on a hot standby. Enabling, or disabling, checksums on the
standby only would cause the cluster to get out of sync and replaying
checksum transitions to fail.
Author: Satyanarayana Narlapuram <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDfRk4-S7DMmdbXJnQ-xF=sUpMAKuh8b83ObLqYVKx5QLA@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 5556a9ca893..ea102086144 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -487,6 +487,8 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
Datum
disable_data_checksums(PG_FUNCTION_ARGS)
{
+ PreventCommandDuringRecovery("pg_disable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -507,6 +509,8 @@ enable_data_checksums(PG_FUNCTION_ARGS)
int cost_delay = PG_GETARG_INT32(0);
int cost_limit = PG_GETARG_INT32(1);
+ PreventCommandDuringRecovery("pg_enable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
--
2.39.3 (Apple Git-146)
[text/plain] POC_test.diff.txt (30.1K, ../../[email protected]/10-POC_test.diff.txt)
download | inline diff:
commit f502fa990e877c6e4d1d03518e524519d8e88806
Author: Tomas Vondra <[email protected]>
Date: Wed Apr 15 12:50:06 2026 +0200
POC: Test checksum state transitions using step through injection points
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index be92b6af20f..5b481784543 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4762,6 +4762,9 @@ SetDataChecksumsOnInProgress(void)
uint32 data_checksum_version;
elog(LOG, "SetDataChecksumsOnInProgress / start");
+
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-delay", NULL);
+
/*
* The state transition is performed in a critical section with
* checkpoints held off to provide crash safety.
@@ -4782,6 +4785,8 @@ SetDataChecksumsOnInProgress(void)
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version %u => %u",
@@ -4791,6 +4796,8 @@ SetDataChecksumsOnInProgress(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOnInProgress / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON);
@@ -4862,6 +4869,8 @@ SetDataChecksumsOn(void)
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-enable-checksums-after-xlogctl", NULL);
+
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
@@ -4875,9 +4884,13 @@ SetDataChecksumsOn(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-enable-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-enable-checksums-after-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOn / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION);
@@ -4924,6 +4937,8 @@ SetDataChecksumsOff(void)
{
SpinLockRelease(&XLogCtl->info_lck);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-delay", NULL);
+
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
@@ -4934,24 +4949,30 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
- elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u = %u",
+ elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
- elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u = %u",
+ elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOff / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF);
@@ -4971,6 +4992,8 @@ SetDataChecksumsOff(void)
SpinLockRelease(&XLogCtl->info_lck);
}
+ INJECTION_POINT("datachecksums-disable-checksums-delay", NULL);
+
START_CRIT_SECTION();
/* Ensure that we don't incur a checkpoint during disabling checksums */
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
@@ -4988,6 +5011,8 @@ SetDataChecksumsOff(void)
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-disable-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -4997,9 +5022,13 @@ SetDataChecksumsOff(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT("datachecksums-disable-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-disable-checksums-after-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOff / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF)");
EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF);
diff --git a/src/test/modules/test_checksums/t/010_injection_2.pl b/src/test/modules/test_checksums/t/010_injection_2.pl
new file mode 100644
index 00000000000..a8ae9ffd151
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_injection_2.pl
@@ -0,0 +1,209 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition. The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - first - first checksum change
+# - second - second checksum change
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers two
+# checksum changes, and verifies the final state is as expected. The first
+# state change is paused on a selected injection point, and unpaused after
+# the second change gets initiated.
+#
+# The injection point is triggered only by the datachecksum launcher, and
+# there can be only one such process. So there's no risk of hitting the
+# injection point by both changes.
+sub test_checksum_transition
+{
+ my ($start, $first, $second, $point, $final) = @_;
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $first . " / " . $second . " / " . $point . " / " . $final . " =========='");
+
+ note($start . " / " . $first . " / " . $second . " / " . $point . " / " . $final);
+
+ note('changing checksums into initial state: ' . $start);
+
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+
+ note("triggering first checksum change: " . $first);
+
+ enable_data_checksums($node) if ($first eq 'enable');
+ disable_data_checksums($node) if ($first eq 'disable');
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+
+ note("triggering second checksum change: " . $second);
+
+ enable_data_checksums($node) if ($second eq 'enable');
+ disable_data_checksums($node) if ($second eq 'disable');
+
+ note("waking and detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');");
+
+ note("detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-checkpoint', 'off');
+
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-delay', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-delay', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-checkpoint', 'on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/011_injection_checkpoint.pl b/src/test/modules/test_checksums/t/011_injection_checkpoint.pl
new file mode 100644
index 00000000000..ce1553e8ecd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/011_injection_checkpoint.pl
@@ -0,0 +1,196 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition concurrent with a checkpoint.
+#
+# The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers a
+# checksum change concurrent with a checkpoint, and verifies the final state
+# is as expected. The state change is paused on a selected injection
+# point, and unpaused after performing a checkpoint.
+#
+# Finally, the instance is restarted (in either fast ot immediate mode),
+# the final checksum state is validated against the expected value, and
+# the server log is checked for checksum failures.
+sub test_checksum_transition
+{
+ my ($start, $change, $point, $final) = @_;
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / " . $point . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / " . $point . " / " . $final);
+
+ note('changing checksums into initial state: ' . $start);
+
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+
+ note("triggering checksum change: " . $change);
+
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+
+ note('checkpoint');
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ note("waking and detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');");
+
+ note("detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+
+ test_checksum_state($node, $final);
+
+ $node->stop(stopmode());
+ $node->start;
+
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl
new file mode 100644
index 00000000000..98e9c805c41
--- /dev/null
+++ b/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl
@@ -0,0 +1,214 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition concurrent with a checkpoint.
+#
+# The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point1 - injection point before checkpoint
+# - point2 - injection point after checkpoint
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers a
+# checksum change that pauses on a selected injection point. Then performs
+# a checkpoint, unpauses the change so that it proceeds to a second
+# injection point.
+#
+# Then the instance is restarted in immediate mode to simulate failure,
+# and the final checksum state is validated against the expected value.
+# The server log is checked for checksum failures.
+sub test_checksum_transition
+{
+ my ($start, $change, $point1, $point2, $final) = @_;
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / " . $point1 . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / " . $point1 . " / " . $final);
+
+ note('changing checksums into initial state: ' . $start);
+
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ note('attaching injection point: ' . $point1);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point1 . "','wait');"
+ );
+
+ if (defined($point2))
+ {
+ note('attaching injection point: ' . $point2);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point2 . "','wait');"
+ );
+ }
+
+ note("triggering checksum change: " . $change);
+
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point1 . "'",
+ '1');
+
+ note('checkpoint');
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ note("waking and detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point1 . "');");
+
+ note("detaching injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point1 . "');");
+
+ if (defined($point2))
+ {
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point2 . "'",
+ '1');
+ }
+ else
+ {
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+ }
+
+ $node->stop('immediate');
+ $node->start;
+
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-delay', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-checkpoint', undef, 'on');
+
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-delay', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-checkpoint', undef, 'off');
+
+$node->stop;
+done_testing();
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-25 11:59 Ayush Tiwari <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Ayush Tiwari @ 2026-04-25 11:59 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>
Hi,
On Thu, 23 Apr 2026 at 15:47, Daniel Gustafsson <[email protected]> wrote:
> While performing extensive post-commit testing using the built-in TAP
> tests in
> checksum_extended mode, Tomas observed a couple issues. The testing was
> done
> on multiple machines, ranging from a fast x86 machine to a much slower
> rpi4,
> but they all performed a very large number of iterations. The combination
> of
> hardware and the large number of tests executed is likely why some of the
> issues went unnoticed.
>
>
Thanks for the patchset. I applied the eight proposed patches on top of
current master and did a review/test pass. I do have one concern
about the duplicate-launcher race discussed earlier, though.
I don't think 0006 fully addresses the duplicate-launcher exit race [1]. I
was
able to reproduce the issue with a deterministic test using the existing
test_checksums delay hooks: delay
the initial StartDataChecksumsWorkerLauncher()
path so two quick pg_enable_data_checksums() calls can queue two launchers,
delay the final transition to on, then fire follow-up
pg_enable_data_checksums()
calls while the real launcher is still active.
The relevant logs from that run:
[37940] DEBUG: background worker "datachecksums launcher" started
[37941] DEBUG: background worker "datachecksums launcher" started
[37940] LOG: enabling data checksums requested, starting data checksum
calculation
[37941] LOG: background worker "datachecksums launcher" already running,
exiting
[37940] LOG: initiating data checksum processing in database "postgres"
[37952] DEBUG: background worker "datachecksums launcher" started
[37952] LOG: enabling data checksums requested, starting data checksum
calculation
[37952] LOG: initiating data checksum processing in database "postgres"
[37952] WARNING: cannot set data checksums to "on", current state is not
"inprogress-on", disabling
[37989] DEBUG: background worker "datachecksums launcher" started
[37989] LOG: enabling data checksums requested, starting data checksum
calculation
[38006] DEBUG: background worker "datachecksums launcher" started
[38006] LOG: enabling data checksums requested, starting data checksum
calculation
So, at least the duplicate-launcher part is still reproducible after the
fixup
series. The warning also suggests the state can be perturbed by the
duplicate
launcher, although in my run the final state still ended up as on.
A few other smaller comments/nits:
1. In src/backend/access/transam/xlog.c, the comment in
SetDataChecksumsOff()
says the branch implies the state is inprogress-on or inprogress-off, but
after the patch inprogress-on is handled by the preceding if condition. It
looks like this should probably only mention inprogress-off.
2. There is a repeated typo in comments:
src/backend/utils/init/postinit.c: "interrups" -> "interrupts"
src/backend/postmaster/auxprocess.c: "interrups" -> "interrupts"
3. A few commit messages could use a quick polish pass:
0002: "onine" -> "online"
0004: "Additionelly" -> "Additionally"
0006: "being enabled of disabled" -> "being enabled or disabled"
0006: "change it's operation" -> "change its operation"
0006: "ss now avoided" -> "is now avoided"
4. One minor question on 0006: the runtime cost-parameter update path is
only
meaningful while checksums are being enabled. That looks fine from the
current
control flow, but would it be worth adding a short comment or assertion near
that update path to make the assumption explicit?
Other than the above concern and cleanup items, the approach and behavior
looked good to me.
Regards,
Ayush
[1] PostgreSQL: [BUG] Race in online checksums launcher_exit()
<https://www.postgresql.org/message-id/CAJTYsWWg6tFrdMhWs5PkwESTNeeUUsMuY17O4UmPPh771c3stA%40mail.gma...;
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-28 20:25 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-04-28 20:25 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Thu, Apr 23, 2026 at 3:16 AM Daniel Gustafsson <[email protected]> wrote:
> While performing extensive post-commit testing using the built-in TAP
> tests in
> checksum_extended mode, Tomas observed a couple issues. The testing was
> done
> on multiple machines, ranging from a fast x86 machine to a much slower
> rpi4,
> but they all performed a very large number of iterations. The combination
> of
> hardware and the large number of tests executed is likely why some of the
> issues went unnoticed.
>
> Below are the issues described in more detail:
>
> 1) race condition between checkpoint and checksum state changes
>
> The main issue is a race condition causing invalid state transitions.
> When analyzing the failure (which we couldn't synthetically reproduce, only
> observe by running tests over and over for a long time) we realized that
> the
> original coding was performing checksum state transitions while replaying
> online checkpoints as well as redo checkpoints.
>
> Updating checksum state while replaying online checkpoints is incorrect.
> After
> a crash we'll start with the REDO record, which initializes checksums to
> the
> right state. And then later the checksum state is updated by the regular
> XLOG_CHECKSUMS entries.
>
> Moreover, the checksum state in the XLOG_CHECKPOINT_ONLINE record can be
> stale,
> because the value is determined at the checkpoint start, but the WAL entry
> is added at the end. If there is a concurrent checksum change, the value
> written to the WAL record will be stale. Replaying it will cause an
> "invalid
> transition" error later, during the next checksum state update. The fix is
> to
> remove the checksum update from the online checkpoint altogether.
>
> In fact, there's no need for CreateCheckPoint to update checksum state
> in the ControlFile. If the value is stale, it could make it permanent.
> But it's unnecessary - the ControlFile is updated by the process
> performing the checksum state change. So remove that.
>
> This, along with an re-ordering updating the controlfile and procsignal
> barrier
> enission fixes the issue. The re-ordering makes sure that the controlfile
> is
> always updated *before* a procsignalbarrier is emitted to avoid a race
> like the
> one described below:
>
> 1. A barrier for off to inprogress-on is emitted
> 2. All active backends absorbs the barrier
> - All processes in the cluster are in state inprogress-on
> 3. A new backend b' forks, reads controlfile and sets state of off
> 4. The controlfile is updated
> 5. A new backend b'' forks, reads controlfile and sets state to
> inprogress-on
>
> b' and b'' have different states, and b' has an incompatible state with the
> rest of the cluster. Re-ordering as done in the attached makes this go
> away.
>
>
> 2) race condition in launcher exit
>
> Another timing related issue was that reverting to the "off" state then
> launcher errors out had synchronization logic which was racy as it was
> relying
> on the cached checksum state and not the value from XLogCtl. The logic for
> determining if a launcher/worker was already active was also fragile as it
> started another launcher which would overwrite certain data in shared
> memory.
> The attached patch inspects shared memory instead and use that to signal
> the
> running launcher to either abort (disable), or change cost parameters on a
> running enable process. These fixes makes erroring out and going to off
> state
> stable.
>
>
> 3) Concurrency issue with ProcSignalInit / InitLocalDataChecksumState
>
> The checksum barriers must not be consumed before the initial value gets
> properly set. On very slow systems, there could potentially be multiple
> checksum state transitions between a fork and InitLocalDataChecksumState.
> In
> such cases we might get failures due to incorrect transitions.
>
> With the current code this is not a live issue, as there is no place
> checking
> interrupts in between the two functions. But it's fragile, as it's
> trivial to
> break this by adding an elog() somewhere. Which is what happened to us
> while
> debugging the other issues. So better to explicitly hold interrupts for a
> brief moment.
>
> To find the issues, and to validate their fix, Tomas developed a new
> testsuite
> which is attached as a .txt. This is not proposed for adding to v19, it is
> included to showcase what was done, and what will be further hacked on for
> a
> new suite during the v20 cycle. It is gated behind PG_TEST_EXTRA and is
> intended to be executed by select members of the buildfarm.
>
> As part of this postcommit review we also found a few more cleanups and
> smaller
> fixes which are included. The patchset also includes the patch submitted
> upthread by Satyanarayana Narlapuram.
>
> More details can be found in the individual commit messages.
>
> Unless there are objections I would like to go ahead with this fixup
> fairly soon.
>
All the patches applied cleanly and the tests passing. A few minor comments:
In SetDataChecksumsOff, stale comment, only INPROGRESS_OFF can reach the
else section
/*
* Ending up here implies that the checksums state is "inprogress-on"
* or "inprogress-off" and we can transition directly to "off" from
* there.
*/
SpinLockRelease(&XLogCtl->info_lck);
Should we update ControlFile->data_checksum_version at the end-of-recovery?
If not, the disk
is stale compared to in memory until the next checkpoint. The code two
lines below updates
the control file anyways to set the DB_IN_PRODUCTION. Maybe combine the
update with that?
It's no big deal if we don't do it because it will be self correct but
tools like pg_controldata
give stale value for some time.
Thanks,
Satya
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-28 22:06 Daniel Gustafsson <[email protected]>
parent: Ayush Tiwari <[email protected]>
0 siblings, 0 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-28 22:06 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>
> On 25 Apr 2026, at 13:59, Ayush Tiwari <[email protected]> wrote:
> I don't think 0006 fully addresses the duplicate-launcher exit race [1]. I was
> able to reproduce the issue with a deterministic test using the existing
> test_checksums delay hooks: delay
> the initial StartDataChecksumsWorkerLauncher()
> path so two quick pg_enable_data_checksums() calls can queue two launchers,
> delay the final transition to on, then fire follow-up pg_enable_data_checksums()
> calls while the real launcher is still active.
Thanks to the really great reproducer script I got offlist (which is attached
here) I was able to reproduce and figure out what was wrong. This sequence
exposed a thinko in the cleanup handler where a process exiting due to an
already running launcher would enter cleanup, and thus wipe state from the
running launcher. It needed this quite narrow window to hit, but it was good
to catch that. Fixed in the attached by giving processes a way to register
themselves as needing no cleanup when exiting.
> A few other smaller comments/nits:
>
> 1. In src/backend/access/transam/xlog.c, the comment in SetDataChecksumsOff()
> says the branch implies the state is inprogress-on or inprogress-off, but
> after the patch inprogress-on is handled by the preceding if condition. It
> looks like this should probably only mention inprogress-off.
Fixed.
> 2. There is a repeated typo in comments:
>
> src/backend/utils/init/postinit.c: "interrups" -> "interrupts"
> src/backend/postmaster/auxprocess.c: "interrups" -> "interrupts"
Fixed.
> 3. A few commit messages could use a quick polish pass:
>
> 0002: "onine" -> "online"
> 0004: "Additionelly" -> "Additionally"
> 0006: "being enabled of disabled" -> "being enabled or disabled"
> 0006: "change it's operation" -> "change its operation"
> 0006: "ss now avoided" -> "is now avoided"
Fixed.
> 4. One minor question on 0006: the runtime cost-parameter update path is only
> meaningful while checksums are being enabled. That looks fine from the current
> control flow, but would it be worth adding a short comment or assertion near
> that update path to make the assumption explicit?
We have this assertion which makes sure that there are no costs passed for
disabling, not sure if we need much more since there is now way for the user to
inject any cost parameters.
#ifdef USE_ASSERT_CHECKING
/* The cost delay settings have no effect when disabling */
if (op == DISABLE_DATACHECKSUMS)
Assert(cost_delay == 0 && cost_limit == 0);
#endif
--
Daniel Gustafsson
#!/usr/bin/env bash
set -euo pipefail
prefix="${PGPREFIX:-$PWD/tmp_install/usr/local/pgsql}"
bindir="$prefix/bin"
sharedir="$prefix/share"
libdir="$prefix/lib"
rows="${ROWS:-80000}"
followup_calls="${FOLLOWUP_CALLS:-5}"
followup_sleep="${FOLLOWUP_SLEEP:-0.2}"
post_run_sleep="${POST_RUN_SLEEP:-8}"
if [[ ! -x "$bindir/initdb" || ! -x "$bindir/pg_ctl" || ! -x "$bindir/psql" ]]; then
cat >&2 <<EOF
could not find PostgreSQL binaries under: $bindir
Set PGPREFIX to the install prefix, or install into:
$PWD/tmp_install/usr/local/pgsql
EOF
exit 1
fi
if [[ ! -f "$sharedir/extension/test_checksums.control" || ! -f "$libdir/test_checksums.so" ]]; then
cat >&2 <<EOF
test_checksums is not installed under: $prefix
Install the test modules first, for example:
make -s -C src/test/modules/injection_points install DESTDIR="$PWD/tmp_install"
make -s -C src/test/modules/test_checksums install DESTDIR="$PWD/tmp_install"
EOF
exit 1
fi
export PATH="$bindir:$PATH"
export LD_LIBRARY_PATH="$libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
base="/tmp/pg-dc-launcher-race-$$"
datadir="$base/data"
logfile="$base/server.log"
port="${PGPORT:-$((55432 + RANDOM % 1000))}"
started=0
mkdir -p "$base"
cleanup()
{
if [[ "$started" -eq 1 ]]; then
pg_ctl -D "$datadir" -m immediate -w stop >"$base/pg_ctl_stop.out" 2>&1 || true
fi
echo "repro artifacts left in: $base"
}
trap cleanup EXIT
initdb --no-data-checksums --auth trust --no-sync --no-instructions -D "$datadir" >"$base/initdb.out"
cat >>"$datadir/postgresql.conf" <<EOF
port = $port
listen_addresses = ''
unix_socket_directories = '$base'
max_worker_processes = 20
log_line_prefix = '%m [%p] '
log_min_messages = debug1
EOF
pg_ctl -D "$datadir" -l "$logfile" -w start >"$base/pg_ctl_start.out"
started=1
psql_args=(-h "$base" -p "$port" -v ON_ERROR_STOP=1 -X postgres)
count_log()
{
grep -F -c "$1" "$logfile" 2>/dev/null || true
}
wait_for_log_count()
{
local needle="$1"
local expected="$2"
local timeout_seconds="$3"
local end=$((SECONDS + timeout_seconds))
local count
while (( SECONDS < end )); do
count=$(count_log "$needle")
if (( count >= expected )); then
return 0
fi
sleep 0.1
done
count=$(count_log "$needle")
echo "timed out waiting for at least $expected occurrence(s) of: $needle" >&2
echo "saw $count occurrence(s)" >&2
return 1
}
psql "${psql_args[@]}" -c "CREATE EXTENSION test_checksums;" >"$base/setup-extension.out"
psql "${psql_args[@]}" -c "CREATE TABLE t AS SELECT g, repeat('x', 2000) AS payload FROM generate_series(1, $rows) g;" >"$base/setup-table.out"
psql "${psql_args[@]}" -c "CHECKPOINT;" >"$base/checkpoint.out"
# Delay the two initial pg_enable_data_checksums() callers at the launcher
# registration entry point, so both callers enter the launch path together.
psql "${psql_args[@]}" -c "SELECT dcw_inject_startup_delay(true);" >"$base/attach-startup-delay.out"
# Delay launcher main before it claims DataChecksumState->launcher_running. This
# makes the same race window deterministic instead of relying on scheduler luck.
psql "${psql_args[@]}" -c "SELECT dcw_inject_launcher_delay(true);" >"$base/attach-launcher-delay.out"
# Delay the final transition to on, keeping the winning launcher active long
# enough for follow-up pg_enable_data_checksums() calls to probe the state.
psql "${psql_args[@]}" -c "SELECT dcw_inject_delay_barrier(true);" >"$base/attach-final-delay.out"
psql "${psql_args[@]}" -c "SELECT pg_enable_data_checksums(20, 1);" >"$base/enable1.out" 2>&1 &
pid1=$!
psql "${psql_args[@]}" -c "SELECT pg_enable_data_checksums(20, 1);" >"$base/enable2.out" 2>&1 &
pid2=$!
wait "$pid1" || true
wait "$pid2" || true
# Later pg_enable_data_checksums() calls should not be delayed at the SQL entry
# point; they need to probe the shared launcher_running state promptly.
psql "${psql_args[@]}" -c "SELECT dcw_inject_startup_delay(false);" >"$base/detach-startup-delay.out"
wait_for_log_count 'background worker "datachecksums launcher" started' 2 15
wait_for_log_count 'background worker "datachecksums launcher" already running, exiting' 1 10
wait_for_log_count 'initiating data checksum processing in database "postgres"' 1 10
# Stop delaying later launchers. Follow-up calls are fired immediately after
# the redundant launcher has exited, while the owning launcher is still
# processing or waiting at the final checksum-on barrier.
psql "${psql_args[@]}" -c "SELECT dcw_inject_launcher_delay(false);" >"$base/detach-launcher-delay.out"
# A single follow-up call is often enough after the redundant launcher exits.
# Use a small handful by default to avoid depending on exact worker timing; set
# FOLLOWUP_CALLS=1 for the smallest variant.
pids=()
for _ in $(seq 1 "$followup_calls"); do
psql "${psql_args[@]}" -c "SELECT pg_enable_data_checksums(20, 1);" >"$base/enable-followup-$_.out" 2>&1 &
pids+=("$!")
sleep "$followup_sleep"
done
for pid in "${pids[@]}"; do
wait "$pid" || true
done
sleep "$post_run_sleep"
echo
echo "--- relevant server log lines ---"
grep -E 'datachecksums launcher|data checksum processing already running|data checksums already in desired state|enabling data checksums requested|initiating data checksum processing|cannot set data checksums|data checksums are now|data checksum processing was aborted|disabling data checksums requested' "$logfile" || true
echo
echo "--- summary ---"
launcher_workers_started=$(count_log 'background worker "datachecksums launcher" started')
checksum_processing_starts=$(count_log 'enabling data checksums requested')
already_running_responses=$(count_log 'data checksum processing already running')
checksum_state_warnings=$(count_log 'cannot set data checksums')
echo "launcher workers started: $launcher_workers_started"
echo "checksum processing starts: $checksum_processing_starts"
echo "already-running responses: $already_running_responses"
echo "checksum state warnings: $checksum_state_warnings"
echo "final data_checksums setting: $(psql "${psql_args[@]}" -At -c 'SHOW data_checksums;' || true)"
echo
if (( checksum_processing_starts > 1 )); then
echo "BUG REPRODUCED: more than one launcher started checksum processing."
else
echo "BUG NOT REPRODUCED: only one launcher started checksum processing."
exit 1
fi
echo
cat <<'EOF'
Expected result on the unfixed patchset:
After the redundant launcher exits, a follow-up call can start another
datachecksums launcher while the original launcher is still active. The log
shows more than one "enabling data checksums requested" line. Depending on
timing, it may also show "cannot set data checksums to \"on\"...".
Expected result with the launcher_exit() ownership fix:
Two launcher workers may start, but the redundant one only logs "already
running, exiting". While the owning launcher is still active, follow-up
calls log "data checksum processing already running"; after it completes,
they may log "data checksums already in desired state, exiting". No
additional launcher starts checksum processing.
EOF
Attachments:
[application/octet-stream] v2-0001-Prevent-pg_enable-disable_data_checksums-on-stand.patch (1.5K, ../../[email protected]/2-v2-0001-Prevent-pg_enable-disable_data_checksums-on-stand.patch)
download | inline diff:
From 80c612cbfffc1115a701f4ef35953608de678ca1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:48:30 +0200
Subject: [PATCH v2 1/8] Prevent pg_enable/disable_data_checksums() on standby
These functions missed a RecoveryInProgress() check, allowing them to
be called on a hot standby. Enabling, or disabling, checksums on the
standby only would cause the cluster to get out of sync and replaying
checksum transitions to fail.
Author: Satyanarayana Narlapuram <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDfRk4-S7DMmdbXJnQ-xF=sUpMAKuh8b83ObLqYVKx5QLA@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 5556a9ca893..ea102086144 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -487,6 +487,8 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
Datum
disable_data_checksums(PG_FUNCTION_ARGS)
{
+ PreventCommandDuringRecovery("pg_disable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -507,6 +509,8 @@ enable_data_checksums(PG_FUNCTION_ARGS)
int cost_delay = PG_GETARG_INT32(0);
int cost_limit = PG_GETARG_INT32(1);
+ PreventCommandDuringRecovery("pg_enable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0002-Test-improvements-for-online-checksums.patch (7.3K, ../../[email protected]/3-v2-0002-Test-improvements-for-online-checksums.patch)
download | inline diff:
From 1d7b0b4db2db1191a1aa89a62965c80633137d16 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:48:39 +0200
Subject: [PATCH v2 2/8] Test improvements for online checksums
This includes a number of smaller fixups to the online checksums test
module which were found during postcommit review and stabilization
work.
* Fix scope increase for PG_TEST_EXTRA: The online checksums tests
have two levels of PG_TEST_EXTRA, checksum and checksums_extended
for extra test runs and test runs with increased randomization.
The logic for increasing the number of test iterations was however
backwards.
* Change stopmode for PITR test: The pitr suite used immediate stop
mode which caused problems on slower machines where the sigquit
would interrupt archive commands leaving partial WAL files behind.
This would then prevent restart. Fix by using fast mode which is
the appropriate mode for the test at hand. Also increase timeouts
to help slower test systems since an expired timeout will incur
the same effect as an immediate standby with a partial WAL left
behind. This issue was observed when running the test suites on
a Raspberry Pi 4 machine.
* Improve logging: The test suite for data checksums use a set of
helper functions in a Perl module to avoid repeating code, this
makes sure that the helper functions do a better job of logging
their test output to make debug easier.
* Remove unused code: wait_for_cluster_crash was used during the
development of online checksums but was never used in any test
which shipped, so remove the function.
* Standby fixes: Ensure no vacuum on pgbench init on standby with
-n to avoid bogus error message in the log, and enable
hot_standby_feedback to prevent queries from getting cancelled
due to recovery on slower systems.
Author: Daniel Gustafsson <[email protected]>
Author: Tomas Vondra <[email protected]>
Discussion: https://postgr.es/m/xxx
---
.../test_checksums/t/007_pgbench_standby.pl | 12 +++--
src/test/modules/test_checksums/t/008_pitr.pl | 5 +-
.../test_checksums/t/DataChecksums/Utils.pm | 53 +++----------------
3 files changed, 20 insertions(+), 50 deletions(-)
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
index f3611e7ce25..0b3996f1d69 100644
--- a/src/test/modules/test_checksums/t/007_pgbench_standby.pl
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -49,8 +49,8 @@ my $node_standby_loglocation = 0;
# of tests performed and the wall time taken is non-deterministic as the test
# performs a lot of randomized actions, but 5 iterations will be a long test
# run regardless.
-my $TEST_ITERATIONS = 5;
-$TEST_ITERATIONS = 1 if ($extended);
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 5 if ($extended);
# Variables which record the current state of the cluster
my $data_checksum_state = 'off';
@@ -83,6 +83,7 @@ sub background_pgbench
push(@cmd, '-C') if ($extended && cointoss());
# If we run on a standby it needs to be a read-only benchmark
push(@cmd, '-S') if ($standby);
+ push(@cmd, '-n') if ($standby);
# Finally add the database name to use
push(@cmd, 'postgres');
@@ -146,8 +147,10 @@ sub flip_data_checksums
. "FROM pg_catalog.pg_settings "
. "WHERE name = 'data_checksums';");
- is(($result eq 'inprogress-on' || $result eq 'on'),
- 1, 'ensure checksums are on, or in progress, on standby_1');
+ is( ($result eq 'inprogress-on' || $result eq 'on'),
+ 1,
+ 'ensure checksums are on, or in progress, on standby_1, got: '
+ . $result);
# Wait for checksums enabled on the primary and standby
wait_for_checksum_state($node_primary, 'on');
@@ -210,6 +213,7 @@ $node_primary->append_conf(
qq[
max_connections = 30
log_statement = none
+hot_standby_feedback = on
]);
$node_primary->start;
$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
index e8cb2b0ed96..1f8176686fd 100644
--- a/src/test/modules/test_checksums/t/008_pitr.pl
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -124,11 +124,14 @@ $node_primary->init(
has_archiving => 1,
allows_streaming => 1,
no_data_checksums => 1);
+my $timeout_unit = 's';
$node_primary->append_conf(
'postgresql.conf',
qq[
max_connections = 100
log_statement = none
+wal_sender_timeout = $PostgreSQL::Test::Utils::timeout_default$timeout_unit
+wal_receiver_timeout = $PostgreSQL::Test::Utils::timeout_default$timeout_unit
]);
$node_primary->start;
@@ -154,7 +157,7 @@ my ($pre_lsn, $post_lsn) = flip_data_checksums();
$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
-$node_primary->stop('immediate');
+$node_primary->stop('fast');
my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
$node_pitr->init_from_backup(
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
index fb704623a60..cb78dd6ecfb 100644
--- a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -43,7 +43,6 @@ our @EXPORT = qw(
stopmode
test_checksum_state
wait_for_checksum_state
- wait_for_cluster_crash
);
=pod
@@ -67,7 +66,10 @@ sub test_checksum_state
my $result = $postgresnode->safe_psql('postgres',
"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
);
- is($result, $state, 'ensure checksums are set to ' . $state);
+ is($result, $state,
+ 'ensure checksums are set to '
+ . $state . ' on '
+ . $postgresnode->name());
return $result eq $state;
}
@@ -89,52 +91,13 @@ sub wait_for_checksum_state
'postgres',
"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
$state);
- is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ is($res, 1,
+ 'ensure data checksums are transitioned to '
+ . $state . ' on '
+ . $postgresnode->name());
return $res == 1;
}
-=item wait_for_cluster_crash(node, params)
-
-Repeatedly test if the cluster running at B<node> responds to connections
-and return when it no longer does so, or when it times out. Processing will
-run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
-value is specified as a parameter. Returns True if the cluster crashed, else
-False if the process timed out.
-
-=over
-
-=item timeout
-
-Approximate number of seconds to wait for cluster to crash, default is
-$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
-that the total process time won't exceed the timeout.
-
-=back
-
-=cut
-
-sub wait_for_cluster_crash
-{
- my $postgresnode = shift;
- my %params = @_;
- my $crash = 0;
-
- $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
- unless (defined($params{timeout}));
-
- for (my $naps = 0; $naps < $params{timeout}; $naps++)
- {
- if (!$postgresnode->is_alive)
- {
- $crash = 1;
- last;
- }
- sleep(1);
- }
-
- return $crash == 1;
-}
-
=item enable_data_checksums($node, %params)
Function for enabling data checksums in the cluster running at B<node>.
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0003-Handle-data_checksum-state-changes-during-launche.patch (6.5K, ../../[email protected]/4-v2-0003-Handle-data_checksum-state-changes-during-launche.patch)
download | inline diff:
From 015b9095011b3e606a5a7e8fbb70936cd7c2c747 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:49:03 +0200
Subject: [PATCH v2 3/8] Handle data_checksum state changes during
launcher_exit
When erroring out from the datachecksums launcher during data checksum
enabling, before state has transitioned to "on", we revert back to the
"off" state. Since checksums weren't enabled, there is no use staying
in an inprogress state since the checksum launcher currently doesn't
support restarting from where it left off. Should restartability get
added in the future, this would need to be revisited. This state
transition was however missing from the allowed transitions in the
statemachine causing an error.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 15 +++---
src/backend/postmaster/datachecksum_state.c | 51 +++++++++++++++++++--
2 files changed, 54 insertions(+), 12 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e39af79c03b..f74d7a2ab1a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4871,13 +4871,14 @@ SetDataChecksumsOff(void)
}
/*
- * If data checksums are currently enabled we first transition to the
- * "inprogress-off" state during which backends continue to write
- * checksums without verifying them. When all backends are in
- * "inprogress-off" the next transition to "off" can be performed, after
- * which all data checksum processing is disabled.
- */
- if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ * If data checksums are currently enabled, or in the process of being
+ * enabled, we first transition to the "inprogress-off" state during which
+ * backends continue to write checksums without verifying them. When all
+ * backends are in "inprogress-off" the next transition to "off" can be
+ * performed, after which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION ||
+ XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
{
SpinLockRelease(&XLogCtl->info_lck);
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index ea102086144..e26803bc501 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -235,7 +235,7 @@ typedef struct ChecksumBarrierCondition
int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[6] =
+static const ChecksumBarrierCondition checksum_barriers[7] =
{
/*
* Disabling checksums: If checksums are currently enabled, disabling must
@@ -261,6 +261,12 @@ static const ChecksumBarrierCondition checksum_barriers[6] =
* checksums, we can go straight back to 'on'
*/
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being enabled when launcher_exit is executed, state
+ * is set to off since we cannot reach on at that point.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
/*
@@ -323,6 +329,13 @@ typedef struct DataChecksumsStateStruct
* catalogs
*/
bool process_shared_catalogs;
+
+ /*
+ * List of PIDs for which the launcher_exit should avoid doing any abort
+ * cleanup, as they are exiting gracefully due to being launched while
+ * another launcher is already running.
+ */
+ List *no_abort;
} DataChecksumsStateStruct;
/* Shared memory segment for datachecksumsworker */
@@ -348,6 +361,7 @@ static DataChecksumsWorkerOperation operation;
/* Prototypes */
static void DataChecksumsShmemRequest(void *arg);
+static void DataChecksumsShmemInit(void *arg);
static bool DatabaseExists(Oid dboid);
static List *BuildDatabaseList(void);
static List *BuildRelationList(bool temp_relations, bool include_shared);
@@ -360,6 +374,7 @@ static void WaitForAllTransactionsToFinish(void);
const ShmemCallbacks DataChecksumsShmemCallbacks = {
.request_fn = DataChecksumsShmemRequest,
+ .init_fn = DataChecksumsShmemInit,
};
/*****************************************************************************
@@ -771,7 +786,9 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
pid_t pid;
char activity[NAMEDATALEN + 64];
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+ LWLockRelease(DataChecksumsWorkerLock);
memset(&bgw, 0, sizeof(bgw));
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
@@ -881,14 +898,29 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
/*
* launcher_exit
*
- * Internal routine for cleaning up state when the launcher process exits. We
- * need to clean up the abort flag to ensure that processing started again if
- * it was previously aborted (note: started again, *not* restarted from where
- * it left off).
+ * Internal routine for cleaning up state when the launcher process exits. If
+ * the process is exiting due to a duplicate started launcher, cleanup should
+ * not be done as that would interfere with the running launcher. Otherwise,
+ * we need to clean up the abort flag to ensure that processing started again
+ * if it was previously aborted (note: started again, *not* restarted from
+ * where it left off).
*/
static void
launcher_exit(int code, Datum arg)
{
+ /* Check for processes which are exiting gracefully */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ foreach_int(dup, DataChecksumState->no_abort)
+ {
+ if (dup == MyProcPid)
+ {
+ DataChecksumState->no_abort = list_delete_int(DataChecksumState->no_abort, MyProcPid);
+ LWLockRelease(DataChecksumsWorkerLock);
+ return;
+ }
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+
abort_requested = false;
if (launcher_running)
@@ -1040,6 +1072,7 @@ DataChecksumsWorkerLauncherMain(Datum arg)
ereport(LOG,
errmsg("background worker \"datachecksums launcher\" already running, exiting"));
/* Launcher was already running, let it finish */
+ DataChecksumState->no_abort = lappend_int(DataChecksumState->no_abort, MyProcPid);
LWLockRelease(DataChecksumsWorkerLock);
return;
}
@@ -1278,6 +1311,14 @@ DataChecksumsShmemRequest(void *arg)
);
}
+static void
+DataChecksumsShmemInit(void *arg)
+{
+ DataChecksumState->no_abort = NIL;
+ DataChecksumState->launcher_running = false;
+ DataChecksumState->worker_pid = InvalidPid;
+}
+
/*
* DatabaseExists
*
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0004-Fix-invalid-checksum-state-transition-in-checkpoi.patch (13.2K, ../../[email protected]/5-v2-0004-Fix-invalid-checksum-state-transition-in-checkpoi.patch)
download | inline diff:
From e3accf52288b20ffba3648d1071fd569673470f6 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:49:05 +0200
Subject: [PATCH v2 4/8] Fix invalid checksum state transition in checkpoints
Commit 78e950cb8 added checksum state handling to all XLOG_CHECKPOINT
records which caused unnecessary state transitions and emission of
procsignal barriers. Remove as only the _REDO record need to handle
checksum state. Barrier emission is also consistently made after
controlfile updates to avoid race conditions.
Additionally, interrupts are held between calling ProcSignalInit and
InitLocalDataChecksumState to remove a window where otherwise invalid
state transitions can happen.
Also remove a pointless assertion on Controlfile which will never hit.
Author: Tomas Vondra <[email protected]>
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 106 ++++++--------------
src/backend/postmaster/auxprocess.c | 10 ++
src/backend/postmaster/datachecksum_state.c | 8 +-
src/backend/utils/init/postinit.c | 10 ++
4 files changed, 55 insertions(+), 79 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f74d7a2ab1a..9b1a10dbd1c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4723,8 +4723,6 @@ SetDataChecksumsOnInProgress(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
/*
* The state transition is performed in a critical section with
* checkpoints held off to provide crash safety.
@@ -4738,25 +4736,16 @@ SetDataChecksumsOnInProgress(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
- /*
- * Update the controlfile before waiting since if we have an immediate
- * shutdown while waiting we want to come back up with checksums enabled.
- */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
UpdateControlFile();
LWLockRelease(ControlFileLock);
- /*
- * Await state change in all backends to ensure that all backends are in
- * "inprogress-on". Once done we know that all backends are writing data
- * checksums.
- */
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
WaitForProcSignalBarrier(barrier);
}
@@ -4787,8 +4776,6 @@ SetDataChecksumsOn(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
SpinLockAcquire(&XLogCtl->info_lck);
/*
@@ -4818,11 +4805,6 @@ SetDataChecksumsOn(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
@@ -4832,12 +4814,12 @@ SetDataChecksumsOn(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
- RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
- /*
- * Await state transition to "on" in all backends. When done we know that
- * data checksums are both written and verified in all backends.
- */
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
WaitForProcSignalBarrier(barrier);
}
@@ -4859,8 +4841,6 @@ SetDataChecksumsOff(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
SpinLockAcquire(&XLogCtl->info_lck);
/* If data checksums are already disabled there is nothing to do */
@@ -4891,22 +4871,17 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
- LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
- ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
- UpdateControlFile();
- LWLockRelease(ControlFileLock);
-
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
-
- /*
- * Update local state in all backends to ensure that any backend in
- * "on" state is changed to "inprogress-off".
- */
WaitForProcSignalBarrier(barrier);
/*
@@ -4935,18 +4910,17 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
- RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
WaitForProcSignalBarrier(barrier);
}
@@ -4961,6 +4935,7 @@ SetDataChecksumsOff(void)
void
InitLocalDataChecksumState(void)
{
+ Assert(InterruptHoldoffCount > 0);
SpinLockAcquire(&XLogCtl->info_lck);
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
@@ -5427,7 +5402,6 @@ XLOGShmemInit(void *arg)
/* Use the checksum info from control file */
XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
-
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
@@ -7518,7 +7492,9 @@ CreateCheckPoint(int flags)
* Get the current data_checksum_version value from xlogctl, valid at the
* time of the checkpoint.
*/
+ SpinLockAcquire(&XLogCtl->info_lck);
checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
if (shutdown)
{
@@ -7639,10 +7615,6 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
- SpinLockAcquire(&XLogCtl->info_lck);
- checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
- SpinLockRelease(&XLogCtl->info_lck);
-
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7792,9 +7764,6 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
- /* make sure we start with the checksum version as of the checkpoint */
- ControlFile->data_checksum_version = checkPoint.dataChecksumState;
-
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -8871,11 +8840,6 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
- SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
- SetLocalDataChecksumState(checkPoint.dataChecksumState);
- SpinLockRelease(&XLogCtl->info_lck);
-
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8936,6 +8900,8 @@ xlog_redo(XLogReaderState *record)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
+ UpdateControlFile();
LWLockRelease(ControlFileLock);
/*
@@ -8962,8 +8928,6 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
- bool new_state = false;
- int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -9002,8 +8966,6 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
- old_state = ControlFile->data_checksum_version;
- ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -9015,18 +8977,6 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
- /*
- * If the data checksum state change we need to emit a barrier.
- */
- SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
- if (checkPoint.dataChecksumState != old_state)
- new_state = true;
- SpinLockRelease(&XLogCtl->info_lck);
-
- if (new_state)
- EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
-
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -9195,6 +9145,7 @@ xlog_redo(XLogReaderState *record)
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ SetLocalDataChecksumState(redo_rec.data_checksum_version);
if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
new_state = true;
SpinLockRelease(&XLogCtl->info_lck);
@@ -9268,6 +9219,11 @@ xlog2_redo(XLogReaderState *record)
XLogCtl->data_checksum_version = state.new_checksum_state;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = state.new_checksum_state;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
/*
* Block on a procsignalbarrier to await all processes having seen the
* change to checksum status. Once the barrier has been passed we can
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 8fdc518b3a1..ba8c9add67a 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -68,6 +68,14 @@ AuxiliaryProcessMainCommon(void)
BaseInit();
+ /*
+ * Prevent consuming interrupts between setting ProcSignalInit and setting
+ * the initial local data checksum value. If a barrier is emitted, and
+ * absorbed, before local cached state is initialized the state transition
+ * can be invalid.
+ */
+ HOLD_INTERRUPTS();
+
ProcSignalInit(NULL, 0);
/*
@@ -88,6 +96,8 @@ AuxiliaryProcessMainCommon(void)
*/
InitLocalDataChecksumState();
+ RESUME_INTERRUPTS();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index e26803bc501..0499cc005b3 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -263,8 +263,8 @@ static const ChecksumBarrierCondition checksum_barriers[7] =
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
/*
- * If checksums are being enabled when launcher_exit is executed, state
- * is set to off since we cannot reach on at that point.
+ * If checksums are being enabled when launcher_exit is executed, state is
+ * set to off since we cannot reach on at that point.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
@@ -1322,8 +1322,8 @@ DataChecksumsShmemInit(void *arg)
/*
* DatabaseExists
*
- * Scans the system catalog to check if a database with the given Oid exist
- * and returns true if it is found, else false.
+ * Scans the system catalog to check if a database with the given Oid exists
+ * and returns true if it is found else false.
*/
static bool
DatabaseExists(Oid dboid)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 6f074013aa9..ecf78b9a986 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -756,6 +756,14 @@ InitPostgres(const char *in_dbname, Oid dboid,
*/
SharedInvalBackendInit(false);
+ /*
+ * Prevent consuming interrupts between setting ProcSignalInit and setting
+ * the initial local data checksum value. If a barrier is emitted, and
+ * absorbed, before local cached state is initialized the state transition
+ * can be invalid.
+ */
+ HOLD_INTERRUPTS();
+
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
/*
@@ -776,6 +784,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
*/
InitLocalDataChecksumState();
+ RESUME_INTERRUPTS();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0005-Typo-and-spelling-fixups-for-online-checksums.patch (3.6K, ../../[email protected]/6-v2-0005-Typo-and-spelling-fixups-for-online-checksums.patch)
download | inline diff:
From 4ceb94bb3dec9e7255c83878367c187eba76ac1a Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:49:28 +0200
Subject: [PATCH v2 5/8] Typo and spelling fixups for online checksums
A collection of spelling, wording and punctuation fixups for the code
documentation from postcommit review.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 5 ++---
src/backend/postmaster/datachecksum_state.c | 12 ++++++------
2 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9b1a10dbd1c..7eb61158f26 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4893,9 +4893,8 @@ SetDataChecksumsOff(void)
else
{
/*
- * Ending up here implies that the checksums state is "inprogress-on"
- * or "inprogress-off" and we can transition directly to "off" from
- * there.
+ * Ending up here implies that the checksums state is "inprogress-off"
+ * and we can transition directly to "off" from there.
*/
SpinLockRelease(&XLogCtl->info_lck);
}
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 0499cc005b3..27cdbb3ed1e 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -86,7 +86,7 @@
* failing to validate the data checksum on the page when reading it.
*
* When processing starts all backends belong to one of the below sets, with
- * one if Bd and Bi being empty:
+ * one of Bd and Bi being empty:
*
* Bg: Backend updating the global state and emitting the procsignalbarrier
* Bd: Backends in "off" state
@@ -286,7 +286,7 @@ typedef struct DataChecksumsStateStruct
int launch_cost_limit;
/*
- * Is a launcher process is currently running? This is set by the main
+ * Is a launcher process currently running? This is set by the main
* launcher process, after it has read the above launch_* parameters.
*/
bool launcher_running;
@@ -325,7 +325,7 @@ typedef struct DataChecksumsStateStruct
DataChecksumsWorkerResult success;
/*
- * tells the worker process whether it should also process the shared
+ * Tells the worker process whether it should also process the shared
* catalogs
*/
bool process_shared_catalogs;
@@ -820,7 +820,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
{
/*
* If the worker managed to start, and stop, before we got to waiting
- * for it we can se a STOPPED status here without it being a failure.
+ * for it we can see a STOPPED status here without it being a failure.
*/
if (DataChecksumState->success == DATACHECKSUMSWORKER_SUCCESSFUL)
{
@@ -1323,7 +1323,7 @@ DataChecksumsShmemInit(void *arg)
* DatabaseExists
*
* Scans the system catalog to check if a database with the given Oid exists
- * and returns true if it is found else false.
+ * and returns true if it is found, else false.
*/
static bool
DatabaseExists(Oid dboid)
@@ -1486,7 +1486,7 @@ BuildRelationList(bool temp_relations, bool include_shared)
/*
* DataChecksumsWorkerMain
*
- * Main function for enabling checksums in a single database, This is the
+ * Main function for enabling checksums in a single database. This is the
* function set as the bgw_function_name in the dynamic background worker
* process initiated for each database by the worker launcher. After enabling
* data checksums in each applicable relation in the database, it will wait for
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0006-Improve-handling-of-concurrent-checksum-requests.patch (9.8K, ../../[email protected]/7-v2-0006-Improve-handling-of-concurrent-checksum-requests.patch)
download | inline diff:
From 6aaf7739d439c99ef7aa8dcd6c916792a2aa1eef Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:49:30 +0200
Subject: [PATCH v2 6/8] Improve handling of concurrent checksum requests
When pg_{enable|disable}_data_checksums is called while checksums are
being enabled or disabled, the already running launcher is detected
and the new desired state is recorded. Processing will then pick up
the new state and change its operation to fulfill the new request.
If the same state is requested but updated cost values, the new cost
values will take effect on the next relation processed. The previous
coding had a complex logic of starting a new launcher for this, which
is now avoided with the shared mem structure instead used to signal
current processing.
This makes the logic more robust, and fixes a bug where the launcher
would erroneously revert back to the "off" state.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/access/transam/xlog.c | 33 +++++++-
src/backend/postmaster/datachecksum_state.c | 85 +++++++++++++++------
src/include/access/xlog.h | 2 +
3 files changed, 96 insertions(+), 24 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 7eb61158f26..757f309016c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4682,10 +4682,41 @@ DataChecksumsNeedWrite(void)
LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
}
+
+bool
+DataChecksumsOff(void)
+{
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_OFF);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
+}
+
+bool
+DataChecksumsOn(void)
+{
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
+}
+
bool
DataChecksumsInProgressOn(void)
{
- return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
}
/*
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 27cdbb3ed1e..be1b0a2d926 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -235,7 +235,7 @@ typedef struct ChecksumBarrierCondition
int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[7] =
+static const ChecksumBarrierCondition checksum_barriers[9] =
{
/*
* Disabling checksums: If checksums are currently enabled, disabling must
@@ -267,6 +267,13 @@ static const ChecksumBarrierCondition checksum_barriers[7] =
* set to off since we cannot reach on at that point.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+
+ /*
+ * Transitions that can happen when a new request is made while another is
+ * currently being processed.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
/*
@@ -377,6 +384,15 @@ const ShmemCallbacks DataChecksumsShmemCallbacks = {
.init_fn = DataChecksumsShmemInit,
};
+#define CHECK_FOR_ABORT_REQUEST() \
+ do { \
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); \
+ if (DataChecksumState->launch_operation != operation) \
+ abort_requested = true; \
+ LWLockRelease(DataChecksumsWorkerLock); \
+ } while (0)
+
+
/*****************************************************************************
* Functionality for manipulating the data checksum state in the cluster
*/
@@ -566,7 +582,6 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
BackgroundWorker bgw;
BackgroundWorkerHandle *bgw_handle;
bool running;
- DataChecksumsWorkerOperation launcher_running_op;
#ifdef USE_ASSERT_CHECKING
/* The cost delay settings have no effect when disabling */
@@ -585,8 +600,6 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
/* Is the launcher already running? If so, what is it doing? */
running = DataChecksumState->launcher_running;
- if (running)
- launcher_running_op = DataChecksumState->operation;
LWLockRelease(DataChecksumsWorkerLock);
@@ -603,13 +616,17 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
* the launcher has had a chance to start up, we still end up launching it
* twice. That's OK, the second invocation will see that a launcher is
* already running and exit quickly.
- *
- * TODO: We could optimize here and skip launching the launcher, if we are
- * already in the desired state, i.e. if the checksums are already enabled
- * and you call pg_enable_data_checksums().
*/
if (!running)
{
+ if ((op == ENABLE_DATACHECKSUMS && DataChecksumsOn()) ||
+ (op == DISABLE_DATACHECKSUMS && DataChecksumsOff()))
+ {
+ ereport(LOG,
+ errmsg("data checksums already in desired state, exiting"));
+ return;
+ }
+
/*
* Prepare the BackgroundWorker and launch it.
*/
@@ -631,9 +648,8 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
}
else
{
- if (launcher_running_op == op)
- ereport(ERROR,
- errmsg("data checksum processing already running"));
+ ereport(LOG,
+ errmsg("data checksum processing already running"));
}
}
@@ -1024,11 +1040,8 @@ WaitForAllTransactionsToFinish(void)
errhint("Data checksums processing must be restarted manually after cluster restart."));
CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
- LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumState->launch_operation != operation)
- abort_requested = true;
- LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
break;
}
@@ -1212,7 +1225,9 @@ ProcessAllDatabases(void)
int cumulative_total = 0;
/* Set up so first run processes shared catalogs, not once in every db */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->process_shared_catalogs = true;
+ LWLockRelease(DataChecksumsWorkerLock);
/* Get a list of all databases to process */
WaitForAllTransactionsToFinish();
@@ -1288,7 +1303,9 @@ ProcessAllDatabases(void)
* When one database has completed, it will have done shared catalogs
* so we don't have to process them again.
*/
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->process_shared_catalogs = false;
+ LWLockRelease(DataChecksumsWorkerLock);
}
FreeDatabaseList(DatabaseList);
@@ -1538,7 +1555,6 @@ DataChecksumsWorkerMain(Datum arg)
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
*/
- Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
VacuumCostDelay = DataChecksumState->cost_delay;
VacuumCostLimit = DataChecksumState->cost_limit;
VacuumCostActive = (VacuumCostDelay > 0);
@@ -1574,8 +1590,6 @@ DataChecksumsWorkerMain(Datum arg)
rels_done = 0;
foreach_oid(reloid, RelationList)
{
- CHECK_FOR_INTERRUPTS();
-
if (!ProcessSingleRelationByOid(reloid, strategy))
{
aborted = true;
@@ -1584,12 +1598,38 @@ DataChecksumsWorkerMain(Datum arg)
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
++rels_done);
+ CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
+
+ if (abort_requested)
+ break;
+
+ /* Check if the cost settings changed during runtime */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
+ || (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
+ {
+ VacuumCostDelay = DataChecksumState->launch_cost_delay;
+ VacuumCostLimit = DataChecksumState->launch_cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+
+ FreeAccessStrategy(strategy);
+ strategy = GetAccessStrategy(BAS_VACUUM);
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+
}
+
list_free(RelationList);
+ FreeAccessStrategy(strategy);
- if (aborted)
+ if (aborted || abort_requested)
{
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ LWLockRelease(DataChecksumsWorkerLock);
ereport(DEBUG1,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1654,15 +1694,14 @@ DataChecksumsWorkerMain(Datum arg)
3000,
WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
- LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- aborted = DataChecksumState->launch_operation != operation;
- LWLockRelease(DataChecksumsWorkerLock);
-
CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
if (aborted || abort_requested)
{
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ LWLockRelease(DataChecksumsWorkerLock);
ereport(LOG,
errmsg("data checksum processing aborted in database OID %u",
dboid));
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 437b4f32349..4dd98624204 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -249,6 +249,8 @@ extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
extern bool DataChecksumsNeedWrite(void);
extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsOn(void);
+extern bool DataChecksumsOff(void);
extern bool DataChecksumsInProgressOn(void);
extern void SetDataChecksumsOnInProgress(void);
extern void SetDataChecksumsOn(void);
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0007-Improve-database-detection-logic-in-datachecksums.patch (2.3K, ../../[email protected]/8-v2-0007-Improve-database-detection-logic-in-datachecksums.patch)
download | inline diff:
From 20e1c46fb33300954e102b37f13753d71180a185 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:50:01 +0200
Subject: [PATCH v2 7/8] Improve database detection logic in
datachecksumsworker
The worker need to know whether a database which failed checksum
processing still exists, or has been dropped. This improves the
detection logic by checking for being partially dropped.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/postmaster/datachecksum_state.c | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index be1b0a2d926..352a0f6139e 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -854,8 +854,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
/*
* Heuristic to see if the database was dropped, and if it was we can
- * treat it as not an error, else treat as fatal and error out. TODO:
- * this could probably be improved with a tighter check.
+ * treat it as not an error, else treat as fatal and error out.
*/
if (DatabaseExists(db->dboid))
return DATACHECKSUMSWORKER_FAILED;
@@ -1340,7 +1339,9 @@ DataChecksumsShmemInit(void *arg)
* DatabaseExists
*
* Scans the system catalog to check if a database with the given Oid exists
- * and returns true if it is found, else false.
+ * and returns true if it is found and valid, else false. Note, we cannot use
+ * database_is_invalid_oid here as it will ERROR out, and we want to gracefully
+ * handle errors.
*/
static bool
DatabaseExists(Oid dboid)
@@ -1350,6 +1351,7 @@ DatabaseExists(Oid dboid)
SysScanDesc scan;
bool found;
HeapTuple tuple;
+ Form_pg_database pg_database_tuple;
StartTransactionCommand();
@@ -1363,6 +1365,14 @@ DatabaseExists(Oid dboid)
tuple = systable_getnext(scan);
found = HeapTupleIsValid(tuple);
+ /* If the Oid exists, ensure that it's not partially dropped */
+ if (found)
+ {
+ pg_database_tuple = (Form_pg_database) GETSTRUCT(tuple);
+ if (database_is_invalid_form(pg_database_tuple))
+ found = false;
+ }
+
systable_endscan(scan);
table_close(rel, AccessShareLock);
--
2.39.3 (Apple Git-146)
[application/octet-stream] v2-0008-Fix-data_checksum-GUC-show_hook.patch (974B, ../../[email protected]/9-v2-0008-Fix-data_checksum-GUC-show_hook.patch)
download | inline diff:
From 59585a0a316f569d847f37addd7022e045d8bfcb Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 28 Apr 2026 23:50:03 +0200
Subject: [PATCH v2 8/8] Fix data_checksum GUC show_hook
Commit f19c0eccae erroneously omitted the show_hook for the
data_checksum GUC.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/xxx
---
src/backend/utils/misc/guc_parameters.dat | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 83af594d4af..afaa058b046 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -585,6 +585,7 @@
variable => 'data_checksums',
boot_val => 'PG_DATA_CHECKSUM_OFF',
options => 'data_checksums_options',
+ show_hook => 'show_data_checksums',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
--
2.39.3 (Apple Git-146)
[text/plain] 0006_bug_test.bash.txt (6.9K, ../../[email protected]/10-0006_bug_test.bash.txt)
download | inline:
#!/usr/bin/env bash
set -euo pipefail
prefix="${PGPREFIX:-$PWD/tmp_install/usr/local/pgsql}"
bindir="$prefix/bin"
sharedir="$prefix/share"
libdir="$prefix/lib"
rows="${ROWS:-80000}"
followup_calls="${FOLLOWUP_CALLS:-5}"
followup_sleep="${FOLLOWUP_SLEEP:-0.2}"
post_run_sleep="${POST_RUN_SLEEP:-8}"
if [[ ! -x "$bindir/initdb" || ! -x "$bindir/pg_ctl" || ! -x "$bindir/psql" ]]; then
cat >&2 <<EOF
could not find PostgreSQL binaries under: $bindir
Set PGPREFIX to the install prefix, or install into:
$PWD/tmp_install/usr/local/pgsql
EOF
exit 1
fi
if [[ ! -f "$sharedir/extension/test_checksums.control" || ! -f "$libdir/test_checksums.so" ]]; then
cat >&2 <<EOF
test_checksums is not installed under: $prefix
Install the test modules first, for example:
make -s -C src/test/modules/injection_points install DESTDIR="$PWD/tmp_install"
make -s -C src/test/modules/test_checksums install DESTDIR="$PWD/tmp_install"
EOF
exit 1
fi
export PATH="$bindir:$PATH"
export LD_LIBRARY_PATH="$libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
base="/tmp/pg-dc-launcher-race-$$"
datadir="$base/data"
logfile="$base/server.log"
port="${PGPORT:-$((55432 + RANDOM % 1000))}"
started=0
mkdir -p "$base"
cleanup()
{
if [[ "$started" -eq 1 ]]; then
pg_ctl -D "$datadir" -m immediate -w stop >"$base/pg_ctl_stop.out" 2>&1 || true
fi
echo "repro artifacts left in: $base"
}
trap cleanup EXIT
initdb --no-data-checksums --auth trust --no-sync --no-instructions -D "$datadir" >"$base/initdb.out"
cat >>"$datadir/postgresql.conf" <<EOF
port = $port
listen_addresses = ''
unix_socket_directories = '$base'
max_worker_processes = 20
log_line_prefix = '%m [%p] '
log_min_messages = debug1
EOF
pg_ctl -D "$datadir" -l "$logfile" -w start >"$base/pg_ctl_start.out"
started=1
psql_args=(-h "$base" -p "$port" -v ON_ERROR_STOP=1 -X postgres)
count_log()
{
grep -F -c "$1" "$logfile" 2>/dev/null || true
}
wait_for_log_count()
{
local needle="$1"
local expected="$2"
local timeout_seconds="$3"
local end=$((SECONDS + timeout_seconds))
local count
while (( SECONDS < end )); do
count=$(count_log "$needle")
if (( count >= expected )); then
return 0
fi
sleep 0.1
done
count=$(count_log "$needle")
echo "timed out waiting for at least $expected occurrence(s) of: $needle" >&2
echo "saw $count occurrence(s)" >&2
return 1
}
psql "${psql_args[@]}" -c "CREATE EXTENSION test_checksums;" >"$base/setup-extension.out"
psql "${psql_args[@]}" -c "CREATE TABLE t AS SELECT g, repeat('x', 2000) AS payload FROM generate_series(1, $rows) g;" >"$base/setup-table.out"
psql "${psql_args[@]}" -c "CHECKPOINT;" >"$base/checkpoint.out"
# Delay the two initial pg_enable_data_checksums() callers at the launcher
# registration entry point, so both callers enter the launch path together.
psql "${psql_args[@]}" -c "SELECT dcw_inject_startup_delay(true);" >"$base/attach-startup-delay.out"
# Delay launcher main before it claims DataChecksumState->launcher_running. This
# makes the same race window deterministic instead of relying on scheduler luck.
psql "${psql_args[@]}" -c "SELECT dcw_inject_launcher_delay(true);" >"$base/attach-launcher-delay.out"
# Delay the final transition to on, keeping the winning launcher active long
# enough for follow-up pg_enable_data_checksums() calls to probe the state.
psql "${psql_args[@]}" -c "SELECT dcw_inject_delay_barrier(true);" >"$base/attach-final-delay.out"
psql "${psql_args[@]}" -c "SELECT pg_enable_data_checksums(20, 1);" >"$base/enable1.out" 2>&1 &
pid1=$!
psql "${psql_args[@]}" -c "SELECT pg_enable_data_checksums(20, 1);" >"$base/enable2.out" 2>&1 &
pid2=$!
wait "$pid1" || true
wait "$pid2" || true
# Later pg_enable_data_checksums() calls should not be delayed at the SQL entry
# point; they need to probe the shared launcher_running state promptly.
psql "${psql_args[@]}" -c "SELECT dcw_inject_startup_delay(false);" >"$base/detach-startup-delay.out"
wait_for_log_count 'background worker "datachecksums launcher" started' 2 15
wait_for_log_count 'background worker "datachecksums launcher" already running, exiting' 1 10
wait_for_log_count 'initiating data checksum processing in database "postgres"' 1 10
# Stop delaying later launchers. Follow-up calls are fired immediately after
# the redundant launcher has exited, while the owning launcher is still
# processing or waiting at the final checksum-on barrier.
psql "${psql_args[@]}" -c "SELECT dcw_inject_launcher_delay(false);" >"$base/detach-launcher-delay.out"
# A single follow-up call is often enough after the redundant launcher exits.
# Use a small handful by default to avoid depending on exact worker timing; set
# FOLLOWUP_CALLS=1 for the smallest variant.
pids=()
for _ in $(seq 1 "$followup_calls"); do
psql "${psql_args[@]}" -c "SELECT pg_enable_data_checksums(20, 1);" >"$base/enable-followup-$_.out" 2>&1 &
pids+=("$!")
sleep "$followup_sleep"
done
for pid in "${pids[@]}"; do
wait "$pid" || true
done
sleep "$post_run_sleep"
echo
echo "--- relevant server log lines ---"
grep -E 'datachecksums launcher|data checksum processing already running|data checksums already in desired state|enabling data checksums requested|initiating data checksum processing|cannot set data checksums|data checksums are now|data checksum processing was aborted|disabling data checksums requested' "$logfile" || true
echo
echo "--- summary ---"
launcher_workers_started=$(count_log 'background worker "datachecksums launcher" started')
checksum_processing_starts=$(count_log 'enabling data checksums requested')
already_running_responses=$(count_log 'data checksum processing already running')
checksum_state_warnings=$(count_log 'cannot set data checksums')
echo "launcher workers started: $launcher_workers_started"
echo "checksum processing starts: $checksum_processing_starts"
echo "already-running responses: $already_running_responses"
echo "checksum state warnings: $checksum_state_warnings"
echo "final data_checksums setting: $(psql "${psql_args[@]}" -At -c 'SHOW data_checksums;' || true)"
echo
if (( checksum_processing_starts > 1 )); then
echo "BUG REPRODUCED: more than one launcher started checksum processing."
else
echo "BUG NOT REPRODUCED: only one launcher started checksum processing."
exit 1
fi
echo
cat <<'EOF'
Expected result on the unfixed patchset:
After the redundant launcher exits, a follow-up call can start another
datachecksums launcher while the original launcher is still active. The log
shows more than one "enabling data checksums requested" line. Depending on
timing, it may also show "cannot set data checksums to \"on\"...".
Expected result with the launcher_exit() ownership fix:
Two launcher workers may start, but the redundant one only logs "already
running, exiting". While the owning launcher is still active, follow-up
calls log "data checksum processing already running"; after it completes,
they may log "data checksums already in desired state, exiting". No
additional launcher starts checksum processing.
EOF
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-29 12:41 Daniel Gustafsson <[email protected]>
parent: SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-29 12:41 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 28 Apr 2026, at 22:25, SATYANARAYANA NARLAPURAM <[email protected]> wrote:
> Should we update ControlFile->data_checksum_version at the end-of-recovery? If not, the disk
> is stale compared to in memory until the next checkpoint. The code two lines below updates
> the control file anyways to set the DB_IN_PRODUCTION. Maybe combine the update with that?
> It's no big deal if we don't do it because it will be self correct but tools like pg_controldata
> give stale value for some time.
I've been thinking about this one and in the end went with adding it. It is
written already with PerformRecoveryXLogAction but we might have an
inprogress-off->off transition after that which seems better to reflect in the
controlfile.
Also, an off-list question on yesterday's bugfix made me realize that it can be
simplified even further. Instead of tracking which processes need cleanup in
the exit handler, we can simply defer installing it at all till we know that
there will be cleanup to process.
The attached also updates the commit messages to reflect the reviewers of this
patchset as well as email id's.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v3-0008-Fix-data_checksum-GUC-show_hook.patch (1.2K, ../../[email protected]/2-v3-0008-Fix-data_checksum-GUC-show_hook.patch)
download | inline diff:
From bf81603b7c30f0ad9665185165d044c55dcda9c0 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:58:33 +0200
Subject: [PATCH v3 8/8] Fix data_checksum GUC show_hook
Commit f19c0eccae erroneously omitted the show_hook for the
data_checksum GUC.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/utils/misc/guc_parameters.dat | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 83af594d4af..afaa058b046 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -585,6 +585,7 @@
variable => 'data_checksums',
boot_val => 'PG_DATA_CHECKSUM_OFF',
options => 'data_checksums_options',
+ show_hook => 'show_data_checksums',
},
# Can't be set by ALTER SYSTEM as it can lead to recursive definition
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0007-Improve-database-detection-logic-in-datachecksums.patch (2.5K, ../../[email protected]/3-v3-0007-Improve-database-detection-logic-in-datachecksums.patch)
download | inline diff:
From 69ed7b3e7f09da35d04064e25f2fa15640949e6f Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:57:53 +0200
Subject: [PATCH v3 7/8] Improve database detection logic in
datachecksumsworker
The worker need to know whether a database which failed checksum
processing still exists, or has been dropped. This improves the
detection logic by checking for being partially dropped.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/postmaster/datachecksum_state.c | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 77f9ab1ffcb..667a061201b 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -845,8 +845,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
/*
* Heuristic to see if the database was dropped, and if it was we can
- * treat it as not an error, else treat as fatal and error out. TODO:
- * this could probably be improved with a tighter check.
+ * treat it as not an error, else treat as fatal and error out.
*/
if (DatabaseExists(db->dboid))
return DATACHECKSUMSWORKER_FAILED;
@@ -1309,7 +1308,9 @@ DataChecksumsShmemRequest(void *arg)
* DatabaseExists
*
* Scans the system catalog to check if a database with the given Oid exists
- * and returns true if it is found, else false.
+ * and returns true if it is found and valid, else false. Note, we cannot use
+ * database_is_invalid_oid here as it will ERROR out, and we want to gracefully
+ * handle errors.
*/
static bool
DatabaseExists(Oid dboid)
@@ -1319,6 +1320,7 @@ DatabaseExists(Oid dboid)
SysScanDesc scan;
bool found;
HeapTuple tuple;
+ Form_pg_database pg_database_tuple;
StartTransactionCommand();
@@ -1332,6 +1334,14 @@ DatabaseExists(Oid dboid)
tuple = systable_getnext(scan);
found = HeapTupleIsValid(tuple);
+ /* If the Oid exists, ensure that it's not partially dropped */
+ if (found)
+ {
+ pg_database_tuple = (Form_pg_database) GETSTRUCT(tuple);
+ if (database_is_invalid_form(pg_database_tuple))
+ found = false;
+ }
+
systable_endscan(scan);
table_close(rel, AccessShareLock);
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0006-Improve-handling-of-concurrent-checksum-requests.patch (10.0K, ../../[email protected]/4-v3-0006-Improve-handling-of-concurrent-checksum-requests.patch)
download | inline diff:
From f9f362f8bda232e9ba8dcb540787831746b4ac28 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:57:04 +0200
Subject: [PATCH v3 6/8] Improve handling of concurrent checksum requests
When pg_{enable|disable}_data_checksums is called while checksums are
being enabled or disabled, the already running launcher is detected
and the new desired state is recorded. Processing will then pick up
the new state and change its operation to fulfill the new request.
If the same state is requested but updated cost values, the new cost
values will take effect on the next relation processed. The previous
coding had a complex logic of starting a new launcher for this, which
is now avoided with the shared mem structure instead used to signal
current processing.
This makes the logic more robust, and fixes a bug where the launcher
would erroneously revert back to the "off" state.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/access/transam/xlog.c | 33 +++++++-
src/backend/postmaster/datachecksum_state.c | 85 +++++++++++++++------
src/include/access/xlog.h | 2 +
3 files changed, 96 insertions(+), 24 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 1409894e506..18d5dee06e0 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4682,10 +4682,41 @@ DataChecksumsNeedWrite(void)
LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF);
}
+
+bool
+DataChecksumsOff(void)
+{
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_OFF);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
+}
+
+bool
+DataChecksumsOn(void)
+{
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
+}
+
bool
DataChecksumsInProgressOn(void)
{
- return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
+ bool ret;
+
+ SpinLockAcquire(&XLogCtl->info_lck);
+ ret = (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON);
+ SpinLockRelease(&XLogCtl->info_lck);
+
+ return ret;
}
/*
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 0f5285f6272..77f9ab1ffcb 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -235,7 +235,7 @@ typedef struct ChecksumBarrierCondition
int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[7] =
+static const ChecksumBarrierCondition checksum_barriers[9] =
{
/*
* Disabling checksums: If checksums are currently enabled, disabling must
@@ -267,6 +267,13 @@ static const ChecksumBarrierCondition checksum_barriers[7] =
* set to off since we cannot reach on at that point.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+
+ /*
+ * Transitions that can happen when a new request is made while another is
+ * currently being processed.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+ {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
/*
@@ -368,6 +375,15 @@ const ShmemCallbacks DataChecksumsShmemCallbacks = {
.request_fn = DataChecksumsShmemRequest,
};
+#define CHECK_FOR_ABORT_REQUEST() \
+ do { \
+ LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); \
+ if (DataChecksumState->launch_operation != operation) \
+ abort_requested = true; \
+ LWLockRelease(DataChecksumsWorkerLock); \
+ } while (0)
+
+
/*****************************************************************************
* Functionality for manipulating the data checksum state in the cluster
*/
@@ -557,7 +573,6 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
BackgroundWorker bgw;
BackgroundWorkerHandle *bgw_handle;
bool running;
- DataChecksumsWorkerOperation launcher_running_op;
#ifdef USE_ASSERT_CHECKING
/* The cost delay settings have no effect when disabling */
@@ -576,8 +591,6 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
/* Is the launcher already running? If so, what is it doing? */
running = DataChecksumState->launcher_running;
- if (running)
- launcher_running_op = DataChecksumState->operation;
LWLockRelease(DataChecksumsWorkerLock);
@@ -594,13 +607,17 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
* the launcher has had a chance to start up, we still end up launching it
* twice. That's OK, the second invocation will see that a launcher is
* already running and exit quickly.
- *
- * TODO: We could optimize here and skip launching the launcher, if we are
- * already in the desired state, i.e. if the checksums are already enabled
- * and you call pg_enable_data_checksums().
*/
if (!running)
{
+ if ((op == ENABLE_DATACHECKSUMS && DataChecksumsOn()) ||
+ (op == DISABLE_DATACHECKSUMS && DataChecksumsOff()))
+ {
+ ereport(LOG,
+ errmsg("data checksums already in desired state, exiting"));
+ return;
+ }
+
/*
* Prepare the BackgroundWorker and launch it.
*/
@@ -622,9 +639,8 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
}
else
{
- if (launcher_running_op == op)
- ereport(ERROR,
- errmsg("data checksum processing already running"));
+ ereport(LOG,
+ errmsg("data checksum processing already running"));
}
}
@@ -1002,11 +1018,8 @@ WaitForAllTransactionsToFinish(void)
errhint("Data checksums processing must be restarted manually after cluster restart."));
CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
- LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
- if (DataChecksumState->launch_operation != operation)
- abort_requested = true;
- LWLockRelease(DataChecksumsWorkerLock);
if (abort_requested)
break;
}
@@ -1189,7 +1202,9 @@ ProcessAllDatabases(void)
int cumulative_total = 0;
/* Set up so first run processes shared catalogs, not once in every db */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->process_shared_catalogs = true;
+ LWLockRelease(DataChecksumsWorkerLock);
/* Get a list of all databases to process */
WaitForAllTransactionsToFinish();
@@ -1265,7 +1280,9 @@ ProcessAllDatabases(void)
* When one database has completed, it will have done shared catalogs
* so we don't have to process them again.
*/
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->process_shared_catalogs = false;
+ LWLockRelease(DataChecksumsWorkerLock);
}
FreeDatabaseList(DatabaseList);
@@ -1507,7 +1524,6 @@ DataChecksumsWorkerMain(Datum arg)
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
*/
- Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
VacuumCostDelay = DataChecksumState->cost_delay;
VacuumCostLimit = DataChecksumState->cost_limit;
VacuumCostActive = (VacuumCostDelay > 0);
@@ -1543,8 +1559,6 @@ DataChecksumsWorkerMain(Datum arg)
rels_done = 0;
foreach_oid(reloid, RelationList)
{
- CHECK_FOR_INTERRUPTS();
-
if (!ProcessSingleRelationByOid(reloid, strategy))
{
aborted = true;
@@ -1553,12 +1567,38 @@ DataChecksumsWorkerMain(Datum arg)
pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
++rels_done);
+ CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
+
+ if (abort_requested)
+ break;
+
+ /* Check if the cost settings changed during runtime */
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+ if ((DataChecksumState->launch_cost_delay != DataChecksumState->cost_delay)
+ || (DataChecksumState->launch_cost_limit != DataChecksumState->cost_limit))
+ {
+ VacuumCostDelay = DataChecksumState->launch_cost_delay;
+ VacuumCostLimit = DataChecksumState->launch_cost_limit;
+ VacuumCostActive = (VacuumCostDelay > 0);
+
+ FreeAccessStrategy(strategy);
+ strategy = GetAccessStrategy(BAS_VACUUM);
+ DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+ DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
+ }
+ LWLockRelease(DataChecksumsWorkerLock);
+
}
+
list_free(RelationList);
+ FreeAccessStrategy(strategy);
- if (aborted)
+ if (aborted || abort_requested)
{
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ LWLockRelease(DataChecksumsWorkerLock);
ereport(DEBUG1,
errmsg("data checksum processing aborted in database OID %u",
dboid));
@@ -1623,15 +1663,14 @@ DataChecksumsWorkerMain(Datum arg)
3000,
WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
- LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
- aborted = DataChecksumState->launch_operation != operation;
- LWLockRelease(DataChecksumsWorkerLock);
-
CHECK_FOR_INTERRUPTS();
+ CHECK_FOR_ABORT_REQUEST();
if (aborted || abort_requested)
{
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
+ LWLockRelease(DataChecksumsWorkerLock);
ereport(LOG,
errmsg("data checksum processing aborted in database OID %u",
dboid));
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 437b4f32349..4dd98624204 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -249,6 +249,8 @@ extern uint64 GetSystemIdentifier(void);
extern char *GetMockAuthenticationNonce(void);
extern bool DataChecksumsNeedWrite(void);
extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsOn(void);
+extern bool DataChecksumsOff(void);
extern bool DataChecksumsInProgressOn(void);
extern void SetDataChecksumsOnInProgress(void);
extern void SetDataChecksumsOn(void);
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0005-Typo-and-spelling-fixups-for-online-checksums.patch (3.8K, ../../[email protected]/5-v3-0005-Typo-and-spelling-fixups-for-online-checksums.patch)
download | inline diff:
From cb22ee74da2a54140853e49e9ed831fff69fd69a Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:56:10 +0200
Subject: [PATCH v3 5/8] Typo and spelling fixups for online checksums
A collection of spelling, wording and punctuation fixups for the code
documentation from postcommit review.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/access/transam/xlog.c | 5 ++---
src/backend/postmaster/datachecksum_state.c | 12 ++++++------
2 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b20c6a2886b..1409894e506 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4893,9 +4893,8 @@ SetDataChecksumsOff(void)
else
{
/*
- * Ending up here implies that the checksums state is "inprogress-on"
- * or "inprogress-off" and we can transition directly to "off" from
- * there.
+ * Ending up here implies that the checksums state is "inprogress-off"
+ * and we can transition directly to "off" from there.
*/
SpinLockRelease(&XLogCtl->info_lck);
}
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 4a8aa5b5ee2..0f5285f6272 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -86,7 +86,7 @@
* failing to validate the data checksum on the page when reading it.
*
* When processing starts all backends belong to one of the below sets, with
- * one if Bd and Bi being empty:
+ * one of Bd and Bi being empty:
*
* Bg: Backend updating the global state and emitting the procsignalbarrier
* Bd: Backends in "off" state
@@ -286,7 +286,7 @@ typedef struct DataChecksumsStateStruct
int launch_cost_limit;
/*
- * Is a launcher process is currently running? This is set by the main
+ * Is a launcher process currently running? This is set by the main
* launcher process, after it has read the above launch_* parameters.
*/
bool launcher_running;
@@ -325,7 +325,7 @@ typedef struct DataChecksumsStateStruct
DataChecksumsWorkerResult success;
/*
- * tells the worker process whether it should also process the shared
+ * Tells the worker process whether it should also process the shared
* catalogs
*/
bool process_shared_catalogs;
@@ -811,7 +811,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
{
/*
* If the worker managed to start, and stop, before we got to waiting
- * for it we can se a STOPPED status here without it being a failure.
+ * for it we can see a STOPPED status here without it being a failure.
*/
if (DataChecksumState->success == DATACHECKSUMSWORKER_SUCCESSFUL)
{
@@ -1292,7 +1292,7 @@ DataChecksumsShmemRequest(void *arg)
* DatabaseExists
*
* Scans the system catalog to check if a database with the given Oid exists
- * and returns true if it is found else false.
+ * and returns true if it is found, else false.
*/
static bool
DatabaseExists(Oid dboid)
@@ -1455,7 +1455,7 @@ BuildRelationList(bool temp_relations, bool include_shared)
/*
* DataChecksumsWorkerMain
*
- * Main function for enabling checksums in a single database, This is the
+ * Main function for enabling checksums in a single database. This is the
* function set as the bgw_function_name in the dynamic background worker
* process initiated for each database by the worker launcher. After enabling
* data checksums in each applicable relation in the database, it will wait for
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0004-Fix-invalid-checksum-state-transition-in-checkpoi.patch (13.6K, ../../[email protected]/6-v3-0004-Fix-invalid-checksum-state-transition-in-checkpoi.patch)
download | inline diff:
From 2103dfdf9e5217d5254c6581234bd9c218483fe5 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:55:27 +0200
Subject: [PATCH v3 4/8] Fix invalid checksum state transition in checkpoints
Commit 78e950cb8 added checksum state handling to all XLOG_CHECKPOINT
records which caused unnecessary state transitions and emission of
procsignal barriers. Remove as only the _REDO record need to handle
checksum state. Barrier emission is also consistently made after
controlfile updates to avoid race conditions.
Additionally, interrupts are held between calling ProcSignalInit and
InitLocalDataChecksumState to remove a window where otherwise invalid
state transitions can happen.
Also remove a pointless assertion on Controlfile which will never hit.
Author: Tomas Vondra <[email protected]>
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/access/transam/xlog.c | 107 ++++++--------------
src/backend/postmaster/auxprocess.c | 10 ++
src/backend/postmaster/datachecksum_state.c | 8 +-
src/backend/utils/init/postinit.c | 10 ++
4 files changed, 56 insertions(+), 79 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f74d7a2ab1a..b20c6a2886b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4723,8 +4723,6 @@ SetDataChecksumsOnInProgress(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
/*
* The state transition is performed in a critical section with
* checkpoints held off to provide crash safety.
@@ -4738,25 +4736,16 @@ SetDataChecksumsOnInProgress(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
- /*
- * Update the controlfile before waiting since if we have an immediate
- * shutdown while waiting we want to come back up with checksums enabled.
- */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
UpdateControlFile();
LWLockRelease(ControlFileLock);
- /*
- * Await state change in all backends to ensure that all backends are in
- * "inprogress-on". Once done we know that all backends are writing data
- * checksums.
- */
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
WaitForProcSignalBarrier(barrier);
}
@@ -4787,8 +4776,6 @@ SetDataChecksumsOn(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
SpinLockAcquire(&XLogCtl->info_lck);
/*
@@ -4818,11 +4805,6 @@ SetDataChecksumsOn(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
@@ -4832,12 +4814,12 @@ SetDataChecksumsOn(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
- RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
- /*
- * Await state transition to "on" in all backends. When done we know that
- * data checksums are both written and verified in all backends.
- */
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
WaitForProcSignalBarrier(barrier);
}
@@ -4859,8 +4841,6 @@ SetDataChecksumsOff(void)
{
uint64 barrier;
- Assert(ControlFile != NULL);
-
SpinLockAcquire(&XLogCtl->info_lck);
/* If data checksums are already disabled there is nothing to do */
@@ -4891,22 +4871,17 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
- LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
- ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
- UpdateControlFile();
- LWLockRelease(ControlFileLock);
-
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
-
- /*
- * Update local state in all backends to ensure that any backend in
- * "on" state is changed to "inprogress-off".
- */
WaitForProcSignalBarrier(barrier);
/*
@@ -4935,18 +4910,17 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SpinLockRelease(&XLogCtl->info_lck);
- barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
-
- MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
- END_CRIT_SECTION();
-
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
- RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+ MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+ END_CRIT_SECTION();
+
+ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
WaitForProcSignalBarrier(barrier);
}
@@ -4961,6 +4935,7 @@ SetDataChecksumsOff(void)
void
InitLocalDataChecksumState(void)
{
+ Assert(InterruptHoldoffCount > 0);
SpinLockAcquire(&XLogCtl->info_lck);
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
@@ -5427,7 +5402,6 @@ XLOGShmemInit(void *arg)
/* Use the checksum info from control file */
XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
-
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
@@ -6650,6 +6624,7 @@ StartupXLOG(void)
ControlFile->state = DB_IN_PRODUCTION;
SpinLockAcquire(&XLogCtl->info_lck);
+ ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->SharedRecoveryState = RECOVERY_STATE_DONE;
SpinLockRelease(&XLogCtl->info_lck);
@@ -7518,7 +7493,9 @@ CreateCheckPoint(int flags)
* Get the current data_checksum_version value from xlogctl, valid at the
* time of the checkpoint.
*/
+ SpinLockAcquire(&XLogCtl->info_lck);
checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+ SpinLockRelease(&XLogCtl->info_lck);
if (shutdown)
{
@@ -7639,10 +7616,6 @@ CreateCheckPoint(int flags)
checkPoint.nextOid += TransamVariables->oidCount;
LWLockRelease(OidGenLock);
- SpinLockAcquire(&XLogCtl->info_lck);
- checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
- SpinLockRelease(&XLogCtl->info_lck);
-
checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
MultiXactGetCheckptMulti(shutdown,
@@ -7792,9 +7765,6 @@ CreateCheckPoint(int flags)
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
- /* make sure we start with the checksum version as of the checkpoint */
- ControlFile->data_checksum_version = checkPoint.dataChecksumState;
-
/*
* Persist unloggedLSN value. It's reset on crash recovery, so this goes
* unused on non-shutdown checkpoints, but seems useful to store it always
@@ -8871,11 +8841,6 @@ xlog_redo(XLogReaderState *record)
MultiXactAdvanceOldest(checkPoint.oldestMulti,
checkPoint.oldestMultiDB);
- SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
- SetLocalDataChecksumState(checkPoint.dataChecksumState);
- SpinLockRelease(&XLogCtl->info_lck);
-
/*
* No need to set oldestClogXid here as well; it'll be set when we
* redo an xl_clog_truncate if it changed since initialization.
@@ -8936,6 +8901,8 @@ xlog_redo(XLogReaderState *record)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+
+ UpdateControlFile();
LWLockRelease(ControlFileLock);
/*
@@ -8962,8 +8929,6 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
- bool new_state = false;
- int old_state;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -9002,8 +8967,6 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
- old_state = ControlFile->data_checksum_version;
- ControlFile->data_checksum_version = checkPoint.dataChecksumState;
LWLockRelease(ControlFileLock);
/* TLI should not change in an on-line checkpoint */
@@ -9015,18 +8978,6 @@ xlog_redo(XLogReaderState *record)
RecoveryRestartPoint(&checkPoint, record);
- /*
- * If the data checksum state change we need to emit a barrier.
- */
- SpinLockAcquire(&XLogCtl->info_lck);
- XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
- if (checkPoint.dataChecksumState != old_state)
- new_state = true;
- SpinLockRelease(&XLogCtl->info_lck);
-
- if (new_state)
- EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState);
-
/*
* After replaying a checkpoint record, free all smgr objects.
* Otherwise we would never do so for dropped relations, as the
@@ -9195,6 +9146,7 @@ xlog_redo(XLogReaderState *record)
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+ SetLocalDataChecksumState(redo_rec.data_checksum_version);
if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
new_state = true;
SpinLockRelease(&XLogCtl->info_lck);
@@ -9268,6 +9220,11 @@ xlog2_redo(XLogReaderState *record)
XLogCtl->data_checksum_version = state.new_checksum_state;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ ControlFile->data_checksum_version = state.new_checksum_state;
+ UpdateControlFile();
+ LWLockRelease(ControlFileLock);
+
/*
* Block on a procsignalbarrier to await all processes having seen the
* change to checksum status. Once the barrier has been passed we can
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 8fdc518b3a1..ba8c9add67a 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -68,6 +68,14 @@ AuxiliaryProcessMainCommon(void)
BaseInit();
+ /*
+ * Prevent consuming interrupts between setting ProcSignalInit and setting
+ * the initial local data checksum value. If a barrier is emitted, and
+ * absorbed, before local cached state is initialized the state transition
+ * can be invalid.
+ */
+ HOLD_INTERRUPTS();
+
ProcSignalInit(NULL, 0);
/*
@@ -88,6 +96,8 @@ AuxiliaryProcessMainCommon(void)
*/
InitLocalDataChecksumState();
+ RESUME_INTERRUPTS();
+
/*
* Auxiliary processes don't run transactions, but they may need a
* resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 77d0316b5cb..4a8aa5b5ee2 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -263,8 +263,8 @@ static const ChecksumBarrierCondition checksum_barriers[7] =
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
/*
- * If checksums are being enabled when launcher_exit is executed, state
- * is set to off since we cannot reach on at that point.
+ * If checksums are being enabled when launcher_exit is executed, state is
+ * set to off since we cannot reach on at that point.
*/
{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
@@ -1291,8 +1291,8 @@ DataChecksumsShmemRequest(void *arg)
/*
* DatabaseExists
*
- * Scans the system catalog to check if a database with the given Oid exist
- * and returns true if it is found, else false.
+ * Scans the system catalog to check if a database with the given Oid exists
+ * and returns true if it is found else false.
*/
static bool
DatabaseExists(Oid dboid)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 6f074013aa9..ecf78b9a986 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -756,6 +756,14 @@ InitPostgres(const char *in_dbname, Oid dboid,
*/
SharedInvalBackendInit(false);
+ /*
+ * Prevent consuming interrupts between setting ProcSignalInit and setting
+ * the initial local data checksum value. If a barrier is emitted, and
+ * absorbed, before local cached state is initialized the state transition
+ * can be invalid.
+ */
+ HOLD_INTERRUPTS();
+
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
/*
@@ -776,6 +784,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
*/
InitLocalDataChecksumState();
+ RESUME_INTERRUPTS();
+
/*
* Also set up timeout handlers needed for backend operation. We need
* these in every case except bootstrap.
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0003-Handle-data_checksum-state-changes-during-launche.patch (5.0K, ../../[email protected]/7-v3-0003-Handle-data_checksum-state-changes-during-launche.patch)
download | inline diff:
From 55f1c2dbc49339cd851f5c1ebc90ec6717dd9c2b Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:54:27 +0200
Subject: [PATCH v3 3/8] Handle data_checksum state changes during
launcher_exit
When erroring out from the datachecksums launcher during data checksum
enabling, before state has transitioned to "on", we revert back to the
"off" state. Since checksums weren't enabled, there is no use staying
in an inprogress state since the checksum launcher currently doesn't
support restarting from where it left off. Should restartability get
added in the future, this would need to be revisited. This state
transition was however missing from the allowed transitions in the
statemachine causing an error.
Author: Daniel Gustafsson <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/access/transam/xlog.c | 15 +++++++-------
src/backend/postmaster/datachecksum_state.c | 22 +++++++++++++++------
2 files changed, 24 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e39af79c03b..f74d7a2ab1a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4871,13 +4871,14 @@ SetDataChecksumsOff(void)
}
/*
- * If data checksums are currently enabled we first transition to the
- * "inprogress-off" state during which backends continue to write
- * checksums without verifying them. When all backends are in
- * "inprogress-off" the next transition to "off" can be performed, after
- * which all data checksum processing is disabled.
- */
- if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+ * If data checksums are currently enabled, or in the process of being
+ * enabled, we first transition to the "inprogress-off" state during which
+ * backends continue to write checksums without verifying them. When all
+ * backends are in "inprogress-off" the next transition to "off" can be
+ * performed, after which all data checksum processing is disabled.
+ */
+ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION ||
+ XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
{
SpinLockRelease(&XLogCtl->info_lck);
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index ea102086144..77d0316b5cb 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -235,7 +235,7 @@ typedef struct ChecksumBarrierCondition
int to;
} ChecksumBarrierCondition;
-static const ChecksumBarrierCondition checksum_barriers[6] =
+static const ChecksumBarrierCondition checksum_barriers[7] =
{
/*
* Disabling checksums: If checksums are currently enabled, disabling must
@@ -261,6 +261,12 @@ static const ChecksumBarrierCondition checksum_barriers[6] =
* checksums, we can go straight back to 'on'
*/
{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
+
+ /*
+ * If checksums are being enabled when launcher_exit is executed, state
+ * is set to off since we cannot reach on at that point.
+ */
+ {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
};
/*
@@ -771,7 +777,9 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
pid_t pid;
char activity[NAMEDATALEN + 64];
+ LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
+ LWLockRelease(DataChecksumsWorkerLock);
memset(&bgw, 0, sizeof(bgw));
bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
@@ -881,10 +889,12 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
/*
* launcher_exit
*
- * Internal routine for cleaning up state when the launcher process exits. We
- * need to clean up the abort flag to ensure that processing started again if
- * it was previously aborted (note: started again, *not* restarted from where
- * it left off).
+ * Internal routine for cleaning up state when a launcher process which has
+ * performed checksum operations exits. A launcher process which is exiting due
+ * to a duplicate started launcher does not need to perform any cleanup and
+ * this function should not be called. Otherwise, we need to clean up the abort
+ * flag to ensure that processing started again if it was previously aborted
+ * (note: started again, *not* restarted from where it left off).
*/
static void
launcher_exit(int code, Datum arg)
@@ -1016,7 +1026,6 @@ WaitForAllTransactionsToFinish(void)
void
DataChecksumsWorkerLauncherMain(Datum arg)
{
- on_shmem_exit(launcher_exit, 0);
ereport(DEBUG1,
errmsg("background worker \"datachecksums launcher\" started"));
@@ -1044,6 +1053,7 @@ DataChecksumsWorkerLauncherMain(Datum arg)
return;
}
+ on_shmem_exit(launcher_exit, 0);
launcher_running = true;
/* Initialize a connection to shared catalogs only */
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0002-Test-improvements-for-online-checksums.patch (7.5K, ../../[email protected]/8-v3-0002-Test-improvements-for-online-checksums.patch)
download | inline diff:
From 9b1390f8aeab18ee0a5d1434f6eb04ed31163c64 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:53:28 +0200
Subject: [PATCH v3 2/8] Test improvements for online checksums
This includes a number of smaller fixups to the online checksums test
module which were found during postcommit review and stabilization
work.
* Fix scope increase for PG_TEST_EXTRA: The online checksums tests
have two levels of PG_TEST_EXTRA, checksum and checksums_extended
for extra test runs and test runs with increased randomization.
The logic for increasing the number of test iterations was however
backwards.
* Change stopmode for PITR test: The pitr suite used immediate stop
mode which caused problems on slower machines where the sigquit
would interrupt archive commands leaving partial WAL files behind.
This would then prevent restart. Fix by using fast mode which is
the appropriate mode for the test at hand. Also increase timeouts
to help slower test systems since an expired timeout will incur
the same effect as an immediate standby with a partial WAL left
behind. This issue was observed when running the test suites on
a Raspberry Pi 4 machine.
* Improve logging: The test suite for data checksums use a set of
helper functions in a Perl module to avoid repeating code, this
makes sure that the helper functions do a better job of logging
their test output to make debug easier.
* Remove unused code: wait_for_cluster_crash was used during the
development of online checksums but was never used in any test
which shipped, so remove the function.
* Standby fixes: Ensure no vacuum on pgbench init on standby with
-n to avoid bogus error message in the log, and enable
hot_standby_feedback to prevent queries from getting cancelled
due to recovery on slower systems.
Author: Daniel Gustafsson <[email protected]>
Author: Tomas Vondra <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reviewed-by: SATYANARAYANA NARLAPURAM <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
.../test_checksums/t/007_pgbench_standby.pl | 12 +++--
src/test/modules/test_checksums/t/008_pitr.pl | 5 +-
.../test_checksums/t/DataChecksums/Utils.pm | 53 +++----------------
3 files changed, 20 insertions(+), 50 deletions(-)
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
index f3611e7ce25..0b3996f1d69 100644
--- a/src/test/modules/test_checksums/t/007_pgbench_standby.pl
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -49,8 +49,8 @@ my $node_standby_loglocation = 0;
# of tests performed and the wall time taken is non-deterministic as the test
# performs a lot of randomized actions, but 5 iterations will be a long test
# run regardless.
-my $TEST_ITERATIONS = 5;
-$TEST_ITERATIONS = 1 if ($extended);
+my $TEST_ITERATIONS = 1;
+$TEST_ITERATIONS = 5 if ($extended);
# Variables which record the current state of the cluster
my $data_checksum_state = 'off';
@@ -83,6 +83,7 @@ sub background_pgbench
push(@cmd, '-C') if ($extended && cointoss());
# If we run on a standby it needs to be a read-only benchmark
push(@cmd, '-S') if ($standby);
+ push(@cmd, '-n') if ($standby);
# Finally add the database name to use
push(@cmd, 'postgres');
@@ -146,8 +147,10 @@ sub flip_data_checksums
. "FROM pg_catalog.pg_settings "
. "WHERE name = 'data_checksums';");
- is(($result eq 'inprogress-on' || $result eq 'on'),
- 1, 'ensure checksums are on, or in progress, on standby_1');
+ is( ($result eq 'inprogress-on' || $result eq 'on'),
+ 1,
+ 'ensure checksums are on, or in progress, on standby_1, got: '
+ . $result);
# Wait for checksums enabled on the primary and standby
wait_for_checksum_state($node_primary, 'on');
@@ -210,6 +213,7 @@ $node_primary->append_conf(
qq[
max_connections = 30
log_statement = none
+hot_standby_feedback = on
]);
$node_primary->start;
$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
index e8cb2b0ed96..1f8176686fd 100644
--- a/src/test/modules/test_checksums/t/008_pitr.pl
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -124,11 +124,14 @@ $node_primary->init(
has_archiving => 1,
allows_streaming => 1,
no_data_checksums => 1);
+my $timeout_unit = 's';
$node_primary->append_conf(
'postgresql.conf',
qq[
max_connections = 100
log_statement = none
+wal_sender_timeout = $PostgreSQL::Test::Utils::timeout_default$timeout_unit
+wal_receiver_timeout = $PostgreSQL::Test::Utils::timeout_default$timeout_unit
]);
$node_primary->start;
@@ -154,7 +157,7 @@ my ($pre_lsn, $post_lsn) = flip_data_checksums();
$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');");
$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
-$node_primary->stop('immediate');
+$node_primary->stop('fast');
my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup');
$node_pitr->init_from_backup(
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
index fb704623a60..cb78dd6ecfb 100644
--- a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -43,7 +43,6 @@ our @EXPORT = qw(
stopmode
test_checksum_state
wait_for_checksum_state
- wait_for_cluster_crash
);
=pod
@@ -67,7 +66,10 @@ sub test_checksum_state
my $result = $postgresnode->safe_psql('postgres',
"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
);
- is($result, $state, 'ensure checksums are set to ' . $state);
+ is($result, $state,
+ 'ensure checksums are set to '
+ . $state . ' on '
+ . $postgresnode->name());
return $result eq $state;
}
@@ -89,52 +91,13 @@ sub wait_for_checksum_state
'postgres',
"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
$state);
- is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+ is($res, 1,
+ 'ensure data checksums are transitioned to '
+ . $state . ' on '
+ . $postgresnode->name());
return $res == 1;
}
-=item wait_for_cluster_crash(node, params)
-
-Repeatedly test if the cluster running at B<node> responds to connections
-and return when it no longer does so, or when it times out. Processing will
-run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
-value is specified as a parameter. Returns True if the cluster crashed, else
-False if the process timed out.
-
-=over
-
-=item timeout
-
-Approximate number of seconds to wait for cluster to crash, default is
-$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees
-that the total process time won't exceed the timeout.
-
-=back
-
-=cut
-
-sub wait_for_cluster_crash
-{
- my $postgresnode = shift;
- my %params = @_;
- my $crash = 0;
-
- $params{timeout} = $PostgreSQL::Test::Utils::timeout_default
- unless (defined($params{timeout}));
-
- for (my $naps = 0; $naps < $params{timeout}; $naps++)
- {
- if (!$postgresnode->is_alive)
- {
- $crash = 1;
- last;
- }
- sleep(1);
- }
-
- return $crash == 1;
-}
-
=item enable_data_checksums($node, %params)
Function for enabling data checksums in the cluster running at B<node>.
--
2.39.3 (Apple Git-146)
[application/octet-stream] v3-0001-Prevent-pg_enable-disable_data_checksums-on-stand.patch (1.7K, ../../[email protected]/9-v3-0001-Prevent-pg_enable-disable_data_checksums-on-stand.patch)
download | inline diff:
From ca2627f2c9529eb57fc89d6e31b66da648976a4c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Wed, 29 Apr 2026 13:52:26 +0200
Subject: [PATCH v3 1/8] Prevent pg_enable/disable_data_checksums() on standby
These functions missed a RecoveryInProgress() check, allowing them to
be called on a hot standby. Enabling, or disabling, checksums on the
standby only would cause the cluster to get out of sync and replaying
checksum transitions to fail.
Author: Satyanarayana Narlapuram <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reviewed-by: Tomas Vondra <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDfRk4-S7DMmdbXJnQ-xF=sUpMAKuh8b83ObLqYVKx5QLA@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 5556a9ca893..ea102086144 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -487,6 +487,8 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
Datum
disable_data_checksums(PG_FUNCTION_ARGS)
{
+ PreventCommandDuringRecovery("pg_disable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -507,6 +509,8 @@ enable_data_checksums(PG_FUNCTION_ARGS)
int cost_delay = PG_GETARG_INT32(0);
int cost_limit = PG_GETARG_INT32(1);
+ PreventCommandDuringRecovery("pg_enable_data_checksums()");
+
if (!superuser())
ereport(ERROR,
errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-29 13:42 Ayush Tiwari <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Ayush Tiwari @ 2026-04-29 13:42 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Wed, 29 Apr 2026 at 18:11, Daniel Gustafsson <[email protected]> wrote:
> > On 28 Apr 2026, at 22:25, SATYANARAYANA NARLAPURAM <
> [email protected]> wrote:
>
> > Should we update ControlFile->data_checksum_version at the
> end-of-recovery? If not, the disk
> > is stale compared to in memory until the next checkpoint. The code two
> lines below updates
> > the control file anyways to set the DB_IN_PRODUCTION. Maybe combine the
> update with that?
> > It's no big deal if we don't do it because it will be self correct but
> tools like pg_controldata
> > give stale value for some time.
>
> I've been thinking about this one and in the end went with adding it. It
> is
> written already with PerformRecoveryXLogAction but we might have an
> inprogress-off->off transition after that which seems better to reflect in
> the
> controlfile.
>
> Also, an off-list question on yesterday's bugfix made me realize that it
> can be
> simplified even further. Instead of tracking which processes need cleanup
> in
> the exit handler, we can simply defer installing it at all till we know
> that
> there will be cleanup to process.
>
> The attached also updates the commit messages to reflect the reviewers of
> this
> patchset as well as email id's.
>
>
Thanks for the updated patchset. I applied v3 on top of current master and
did
a focused review/test pass.
Reran the duplicate-launcher reproducer three times against v3 with a more
aggressive variant, could not see any issue.
The v3 launcher_exit change looks good to me. Deferring installation of the
on_shmem_exit() handler until after the process has confirmed that it owns
the
launcher role avoids the duplicate-launcher cleanup problem.
I also looked at the end-of-recovery control-file update. Copying
XLogCtl->data_checksum_version into ControlFile->data_checksum_version under
ControlFileLock before the existing UpdateControlFile() call is a good idea.
The patchset looks good.
Regards,
Ayush
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-04-30 15:20 Daniel Gustafsson <[email protected]>
parent: Ayush Tiwari <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-04-30 15:20 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 29 Apr 2026, at 15:42, Ayush Tiwari <[email protected]> wrote:
> The patchset looks good.
Thanks for review, I've applied the patchset after some editorializing.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-01 16:57 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-05-01 16:57 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi Daniel,
On Thu, Apr 30, 2026 at 8:20 AM Daniel Gustafsson <[email protected]> wrote:
> > On 29 Apr 2026, at 15:42, Ayush Tiwari <[email protected]>
> wrote:
>
> > The patchset looks good.
>
> Thanks for review, I've applied the patchset after some editorializing.
>
While further testing this feature, I realized that
ProcessSingleRelationFork()
unconditionally called log_newpage_buffer() for every page of every
relation
during pg_enable_data_checksums(). This included unlogged relations,
which by definition never generate WAL for data changes and are reset to
their
init fork on any recovery.
Guard the log_newpage_buffer() call with RelationNeedsWAL() so that
unlogged relations still get their pages dirtied (ensuring the checksum
is flushed to disk at the next checkpoint) but do not emit WAL.
Attached a patch to address this and added a test for the same. My current
test checks if standby has main fork, I could just checked WAL to verify
this
using pg_waldump. Any other test ideas are welcome.
Thanks,
Satya
Attachments:
[application/octet-stream] 0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (4.8K, ../../CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com/3-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch)
download | inline diff:
From b54e3dbf7455333a941e2b16a49520b02f7d8b52 Mon Sep 17 00:00:00 2001
From: Satya Narlapuram <[email protected]>
Date: Fri, 1 May 2026 15:37:51 +0000
Subject: [PATCH] Skip WAL for unlogged relations during online checksum enable
ProcessSingleRelationFork() unconditionally called log_newpage_buffer()
for every page of every relation during pg_enable_data_checksums().
This included unlogged relations, which by definition never generate
WAL for data changes and are reset to their init fork on any recovery.
FPI WAL records are wasteful on the primary (proportional
to the total size of all unlogged tables) and cause the standby to
pointlessly materialize data files that should not exist there.
Guard the log_newpage_buffer() call with RelationNeedsWAL() so that
unlogged relations still get their pages dirtied (ensuring the checksum
is flushed to disk at the next checkpoint) but do not emit WAL.
Add a test to 003_standby_restarts.pl that creates an unlogged table,
enables checksums, and verifies via pg_relation_size() on the standby
that no data was materialized for the unlogged relation after replay.
---
src/backend/postmaster/datachecksum_state.c | 9 +++-
.../test_checksums/t/003_standby_restarts.pl | 45 +++++++++++++++++++
2 files changed, 52 insertions(+), 2 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index d0d6acdd..535da8b0 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -690,11 +690,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
* at one point in the past, so only when checksums are first on, then
* off, and then turned on again. TODO: investigate if this could be
* avoided if the checksum is calculated to be correct and wal_level
- * is set to "minimal",
+ * is set to "minimal".
+ *
+ * Unlogged relations don't need WAL since they are reset to their
+ * init fork on recovery. We still dirty the buffer so that the
+ * checksum is written to disk at the next checkpoint.
*/
START_CRIT_SECTION();
MarkBufferDirty(buf);
- log_newpage_buffer(buf, false);
+ if (RelationNeedsWAL(reln))
+ log_newpage_buffer(buf, false);
END_CRIT_SECTION();
UnlockReleaseBuffer(buf);
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index 11e15c9d..858329b6 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -115,6 +115,51 @@ $result =
$node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
is($result, "19998", 'ensure we can safely read all data without checksums');
+# ---------------------------------------------------------------------------
+# Test that enabling checksums does not emit WAL for unlogged relations.
+# Unlogged relations are wiped on recovery, so FPIs for them would be
+# pointless and waste WAL traffic / standby I/O.
+#
+
+$node_primary->safe_psql('postgres',
+ "CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,10000) AS a;");
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Get the relfilenode and database OID so we can search the WAL for it
+my $unlogged_rfn = $node_primary->safe_psql('postgres',
+ "SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';");
+my $db_oid = $node_primary->safe_psql('postgres',
+ "SELECT oid FROM pg_database WHERE datname = 'postgres';");
+
+# Verify the standby only has the init fork (no main fork)
+my $standby_datadir = $node_standby->data_dir;
+ok(!-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+ 'standby has no main fork for unlogged table before enable');
+
+# Re-enable data checksums
+enable_data_checksums($node_primary, wait => 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+# After standby replays, the unlogged main file must still not exist.
+# If the bug were present, FPI replay would materialize the full table.
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+ok(!-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+ 'standby has no main fork for unlogged table after enable');
+
+# Verify unlogged relation size is 0 on the standby (main fork missing)
+my $standby_size = $node_standby->safe_psql('postgres',
+ "SELECT pg_relation_size('unlogged_tbl', 'main');");
+is($standby_size, '0',
+ 'unlogged table has zero size on standby after checksum enable');
+
+# Unlogged table should still be readable on primary
+$result = $node_primary->safe_psql('postgres',
+ 'SELECT count(*) FROM unlogged_tbl;');
+is($result, '10000',
+ 'unlogged table readable on primary after checksum enable');
+
$node_standby->stop;
$node_primary->stop;
done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-01 21:13 Daniel Gustafsson <[email protected]>
parent: SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-05-01 21:13 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 1 May 2026, at 18:57, SATYANARAYANA NARLAPURAM <[email protected]> wrote:
> While further testing this feature, I realized that ProcessSingleRelationFork()
> unconditionally called log_newpage_buffer() for every page of every relation
> during pg_enable_data_checksums(). This included unlogged relations,
> which by definition never generate WAL for data changes and are reset to their
> init fork on any recovery.
>
> Guard the log_newpage_buffer() call with RelationNeedsWAL() so that
> unlogged relations still get their pages dirtied (ensuring the checksum
> is flushed to disk at the next checkpoint) but do not emit WAL.
>
> Attached a patch to address this and added a test for the same. My current
> test checks if standby has main fork, I could just checked WAL to verify this
> using pg_waldump. Any other test ideas are welcome.
Thanks for the report, I agree that this is an oversight that should be fixed.
Your patch looks good on first glance, I am travelling till Sunday evening so
will take another look when back in the office and will apply it then. Thanks!
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-04 13:16 Tomas Vondra <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 3 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-05-04 13:16 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
Thanks for getting this feature pushed, and for resolving the failures
reported since the feature freeze. I consider this to be an important
improvement, not just for the feature itself, but also because of all
the useful infrastructure it added.
Attached is a refined version of the TAP tests already posted by Daniel
some time ago [1]. Unfortunately, that .txt did not apply cleanly for
some reason, so here's a better version.
I found these tests quite useful when reasoning about how the patch
behaves in concurrent environment (e.g. with multiple sessions
triggering checksum enable/disable, or with a checkpoint, crashes, etc).
At this point all the tests pass, but there are a couple cases with
correct but slightly surprising behavior, worth discussing. Which is
what this e-mail is going to be about.
I'll explain what the TAP tests aim to do first, and then discuss the
slightly surprising behavior.
It's not meant for inclusion into PG19, at least not in this shape - I
wrote those TAP tests while investigating some of the earlier failures
and/or when wondering about behavior in various situations (sequence of
concurrent steps, race conditions, ...). So it's more of an exhaustive,
and the tests are somewhat redundant (N+1 is often just (N + some small
tweak)).
I can imagine distilling it into a tiny subset, and adding that. But
that's up to discussion. But that's for later.
Let me briefly explain what the various TAP tests aim to do. From the
very beginning, my main concern regarding this patch was race conditions
when updating the shared state about effective data_checksum_version.
Because the state is effectively split into about three or four places:
* LocalDataChecksumVersion (local cache)
* XLogCtl->data_checksum_version (XLogCtl->info_lck)
* ControlFile->data_checksum_version (ControlFileLock)
* state in control file on disk
These pieces are protected by different locks, the protocol for updating
and/or reading the various flags is not trivial (and some of the fixed
issues were due to ControlFile->data_checksum_version being updated from
a place that shouldn't have).
So the primary goal of the TAP tests was to check for race conditions by
leveraging injection points to step through concurrent processes in a
deterministic way. The first couple patches (0001-0004) add debug
logging and injection points into a lot of places. And by "a lot" I mean
~80 new injection points, which is about the number of injection points
we have in master now. Anyway, this allows stepping through concurrent
checksum changes, and also checksum change vs. checkpointer.
Then come the actual TAP tests:
1) 0005-TAP-10-concurrent-checksum-changes.patch
Two concurrent checksum changes. The first one gets paused at an
injection point, then the second one gets initiated.
2) 0006-TAP-11-concurrency-with-checkpoints.patch
A checksum change + checkpoint. The change gets paused at an injection
point, a synchronous checkpoint is performed.
3) 0007-TAP-12-crashes-at-injection-points.patch
Similar to 0006, but with a crash + recovery. A checksum change gets
paused at an injection point, a synchronous checkpoint is performed. The
changes gets wpken up and either completes, or pauses on a different
injection point. A restart/crash happens.
4) 0008-TAP-13-concurrency-with-checkpoint-REDO.patch
Similar to 0007, but the checkpoint is not synchronous - happens in the
background, so that the TAP can step through both sides and interleave
them in an arbitrary way. This matters because the checksum change
updates the different state pieces (XlogCtl/ControlFile), while the
checkpointer reads them to record initial state for REDO etc.
5) 0009-TAP-14-checkpoints-with-crashes.patch
Similar to 0008, except that the steps are more fine grained, and
focused on two particular cases with surprisingly different final state.
AFAIK everything works as expected, except for two cases in the "TAP
012" test. One for the "enabling" direction, one for the "disabling"
direction. I'm going to discuss the "enabling" direction, I believe the
other case is just a mirror with the same root cause.
The TAP 012 tests checksum change with a concurrent checkpoint, followed
by a crash, and tests the final state. It pauses the change at an
injection point, does a checkpoint, proceeds to the next injection
point, crashes and does recovery. The expectation is that the final
state "flips" at some injection point, once it gets further enough, and
stays there. But what actually happens is this:
a) test_checksum_transition(
'disabled', 'enable', undef,
'datachecksums-enable-inprogress-checksums-end',
'datachecksums-enable-checksums-start',
'off');
b) test_checksum_transition(
'disabled', 'enable', undef,
'datachecksums-enable-checksums-start',
'datachecksums-enable-checksums-after-xlog',
'on');
c) test_checksum_transition(
'disabled', 'enable', 'datachecksums-enable-checksums-start',
'datachecksums-enable-checksums-after-xlogs',
'datachecksums-enable-checksums-after-xlogctl',
'off');
This says that if the checkpoint happens after
'datachecksums-enable-inprogress-checksums-end' or after
'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e.
enabling checksums fails).
But if the checkpoint happens after
'datachecksums-enable-checksums-start', we end up with "on" (after
recovery).
This is a bit surprising, because that injection point is before
'datachecksums-enable-checksums-after-xlog'. So the enabling process
gets further and further, but the final state flips off -> on -> off,
contradicting the expectation that it changes once.
I haven't quite wrapped my head around it yet, but my understanding is
this is due to a race condition between the checksum launcher (writing
XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer
(reading the shmem state and generating REDO).
The launcher does this sequence of steps:
1) write XLOG2_CHECKSUMS with new state
2) update XLogCtl->data_checksum_version
3) update ControlFile->data_checksum_version
4) UpdateControlFile()
5) emits barrier
while the checkpointer (CreateCheckPoint) does this:
A) read XLogCtl->data_checksum_version (while holding insert locks)
B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
C) UpdateControlFile()
The outcome depends on how exactly these two sequences interleave. For
example, this can happen:
1) write XLOG2_CHECKSUMS with new state
A) read XLogCtl->data_checksum_version (while holding insert locks)
B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
C) UpdateControlFile()
2) update XLogCtl->data_checksum_version
3) update ControlFile->data_checksum_version
4) UpdateControlFile()
5) emits barrier
Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and
so redo won't see it), but the checkpoint will still get the old
checksum state from XLogCtl. And so the outcome is "off", per case (c).
But it can also happen what case (b) does:
A) read XLogCtl->data_checksum_version (while holding insert locks)
B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
C) UpdateControlFile()
1) write XLOG2_CHECKSUMS with new state
2) update XLogCtl->data_checksum_version
3) update ControlFile->data_checksum_version
4) UpdateControlFile()
5) emits barrier
In which case the REDO will have the old state, but the recovery will
read the XLOG2_CHECKSUMS, and so end up with "on".
This is the root cause of the surprising behavior in TAP 012, I think.
I attempted to trigger these race conditions in TAP 013, but without
much success. In the end I realized it probably needs more control,
waiting for the other process to hit the next injection point before
unpausing the current one. TAP 014 does that, and it shows that with the
right interleaving of steps the (c) case can end up with both "on" and
"off" final state.
As I said, I don't claim I fully understand this yet. But I wouldn't
call this "bug" - AFAICS it won't produce an incorrect final state (I
haven't seen any such cases).
Still, I wonder if there's a potential issue I failed to notice.
The other question I had when looking at this (concurrency with
checkpoints) is what we get by doing
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
whenever updating the state in SetDataChecksums... functions. Because
the only thing that guarantees is the updates happen on one side of the
checkpoint record. What does that give us, actually?
It does not seem to prevent this surprising behavior, and it does not
say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO.
regards
[1]
https://www.postgresql.org/message-id/9197F930-DDEB-4CAC-82A2-16FEC715CCE8%40yesql.se
--
Tomas Vondra
Attachments:
[text/x-patch] 0001-debug-checkpoint-logging.patch (1.2K, ../../[email protected]/2-0001-debug-checkpoint-logging.patch)
download | inline diff:
From 696077acf842ce1de0bba320b10c18e5c22870e1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Mon, 13 Apr 2026 14:36:43 +0200
Subject: [PATCH 01/13] debug: checkpoint logging
---
src/backend/access/transam/xlog.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 18d5dee06e0..94b7afeb6c7 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7568,6 +7568,9 @@ CreateCheckPoint(int flags)
*/
WALInsertLockRelease();
+ elog(LOG, "CreateCheckPoint start redo %X/%X checksums %d",
+ LSN_FORMAT_ARGS(RedoRecPtr), checkPoint.dataChecksumState);
+
/*
* If this is an online checkpoint, we have not yet determined the redo
* point. We do so now by inserting the special XLOG_CHECKPOINT_REDO
@@ -7811,6 +7814,10 @@ CreateCheckPoint(int flags)
*/
END_CRIT_SECTION();
+ elog(LOG, "CreateCheckPoint end lsn %X/%X redo %X/%X checksums %d",
+ LSN_FORMAT_ARGS(recptr), LSN_FORMAT_ARGS(RedoRecPtr),
+ checkPoint.dataChecksumState);
+
/*
* WAL summaries end when the next XLOG_CHECKPOINT_REDO or
* XLOG_CHECKPOINT_SHUTDOWN record is reached. This is the first point
--
2.54.0
[text/x-patch] 0002-debug-state-transitions.patch (14.2K, ../../[email protected]/3-0002-debug-state-transitions.patch)
download | inline diff:
From 15c2237b02cbf93558f6f8110f046acce976e0e4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Wed, 8 Apr 2026 22:07:21 +0200
Subject: [PATCH 02/13] debug: state transitions
---
src/backend/access/transam/xlog.c | 125 ++++++++++++++++++++++++++++++
1 file changed, 125 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 94b7afeb6c7..72110d92c8b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4288,12 +4288,14 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
ControlFile->wal_log_hints = wal_log_hints;
ControlFile->track_commit_timestamp = track_commit_timestamp;
ControlFile->data_checksum_version = data_checksum_version;
+ elog(LOG, "InitControlFile ControlFile->data_checksum_version = %u", data_checksum_version);
/*
* Set the data_checksum_version value into XLogCtl, which is where all
* processes get the current value from.
*/
XLogCtl->data_checksum_version = data_checksum_version;
+ elog(LOG, "InitControlFile XLogCtl->data_checksum_version = %u", data_checksum_version);
}
static void
@@ -4753,6 +4755,9 @@ void
SetDataChecksumsOnInProgress(void)
{
uint64 barrier;
+ uint32 data_checksum_version;
+
+ elog(LOG, "SetDataChecksumsOnInProgress / start");
/*
* The state transition is performed in a critical section with
@@ -4764,20 +4769,32 @@ SetDataChecksumsOnInProgress(void)
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+ elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version %u => %u",
+ ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
+
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ elog(LOG, "SetDataChecksumsOnInProgress / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ elog(LOG, "SetDataChecksumsOnInProgress / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
WaitForProcSignalBarrier(barrier);
+
+ elog(LOG, "SetDataChecksumsOnInProgress / end");
}
/*
@@ -4806,6 +4823,9 @@ void
SetDataChecksumsOn(void)
{
uint64 barrier;
+ uint32 data_checksum_version;
+
+ elog(LOG, "SetDataChecksumsOn / start");
SpinLockAcquire(&XLogCtl->info_lck);
@@ -4833,25 +4853,39 @@ SetDataChecksumsOn(void)
XLogChecksums(PG_DATA_CHECKSUM_VERSION);
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, PG_DATA_CHECKSUM_VERSION);
+
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
*/
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+ elog(LOG, "SetDataChecksumsOn / ControlFile->data_checksum_version %u => %u",
+ ControlFile->data_checksum_version, PG_DATA_CHECKSUM_VERSION);
+
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ elog(LOG, "SetDataChecksumsOn / EmitProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ elog(LOG, "SetDataChecksumsOn / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
WaitForProcSignalBarrier(barrier);
+
+ elog(LOG, "SetDataChecksumsOn / end");
}
/*
@@ -4871,6 +4905,9 @@ void
SetDataChecksumsOff(void)
{
uint64 barrier;
+ uint32 data_checksum_version;
+
+ elog(LOG, "SetDataChecksumsOff / start");
SpinLockAcquire(&XLogCtl->info_lck);
@@ -4899,20 +4936,32 @@ SetDataChecksumsOff(void)
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+ elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
+ ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
WaitForProcSignalBarrier(barrier);
/*
@@ -4937,21 +4986,35 @@ SetDataChecksumsOff(void)
XLogChecksums(PG_DATA_CHECKSUM_OFF);
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+ elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
+ ControlFile->data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+ elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
WaitForProcSignalBarrier(barrier);
+
+ elog(LOG, "SetDataChecksumsOff / end");
}
/*
@@ -4966,9 +5029,14 @@ void
InitLocalDataChecksumState(void)
{
Assert(InterruptHoldoffCount > 0);
+
+ elog(LOG, "InitLocalDataChecksumState start %d", LocalDataChecksumState);
+
SpinLockAcquire(&XLogCtl->info_lck);
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
+
+ elog(LOG, "InitLocalDataChecksumState end %d", LocalDataChecksumState);
}
void
@@ -5278,7 +5346,9 @@ LocalProcessControlFile(bool reset)
LocalControlFile = palloc_object(ControlFileData);
ControlFile = LocalControlFile;
ReadControlFile();
+ elog(LOG, "LocalProcessControlFile start %d", LocalDataChecksumState);
SetLocalDataChecksumState(ControlFile->data_checksum_version);
+ elog(LOG, "LocalProcessControlFile end %d", LocalDataChecksumState);
}
/*
@@ -5430,9 +5500,15 @@ XLOGShmemInit(void *arg)
XLogCtl->InstallXLogFileSegmentActive = false;
XLogCtl->WalWriterSleeping = false;
+ elog(LOG, "XLOGShmemInit / XLogCtl->data_checksum_version %u => %u",
+ XLogCtl->data_checksum_version, ControlFile->data_checksum_version);
+
/* Use the checksum info from control file */
XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+ elog(LOG, "XLOGShmemInit start %d", LocalDataChecksumState);
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+ elog(LOG, "XLOGShmemInit end %d", LocalDataChecksumState);
SpinLockInit(&XLogCtl->Insert.insertpos_lck);
SpinLockInit(&XLogCtl->info_lck);
@@ -6607,13 +6683,22 @@ StartupXLOG(void)
*/
if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
{
+ uint32 data_checksum_version;
XLogChecksums(PG_DATA_CHECKSUM_OFF);
+ elog(LOG, "StartupXLOG start %d", LocalDataChecksumState);
+
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "StartupXLOG XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
+ elog(LOG, "StartupXLOG end %d", LocalDataChecksumState);
+
ereport(WARNING,
errmsg("enabling data checksums was interrupted"),
errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
@@ -6627,12 +6712,21 @@ StartupXLOG(void)
*/
if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
{
+ uint32 data_checksum_version;
XLogChecksums(PG_DATA_CHECKSUM_OFF);
+ elog(LOG, "StartupXLOG start %d", LocalDataChecksumState);
+
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
+
+ elog(LOG, "StartupXLOG XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
+ elog(LOG, "StartupXLOG end %d", LocalDataChecksumState);
}
/*
@@ -7917,6 +8011,8 @@ CreateEndOfRecoveryRecord(void)
{
xl_end_of_recovery xlrec;
XLogRecPtr recptr;
+ uint32 old_data_checksum_version;
+ uint32 new_data_checksum_version;
/* sanity check */
if (!RecoveryInProgress())
@@ -7948,9 +8044,14 @@ CreateEndOfRecoveryRecord(void)
/* start with the latest checksum version (as of the end of recovery) */
SpinLockAcquire(&XLogCtl->info_lck);
+ old_data_checksum_version = ControlFile->data_checksum_version;
+ new_data_checksum_version = XLogCtl->data_checksum_version;
ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "CreateEndOfRecoveryRecord ControlFile->data_checksum_version %u => %u",
+ old_data_checksum_version, new_data_checksum_version);
+
UpdateControlFile();
LWLockRelease(ControlFileLock);
@@ -8293,6 +8394,9 @@ CreateRestartPoint(int flags)
ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
}
+ elog(LOG, "CreateRestartPoint ControlFile->data_checksum_version %u => %u",
+ ControlFile->data_checksum_version, lastCheckPoint.dataChecksumState);
+
/* we shall start with the latest checksum version */
ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
@@ -8750,6 +8854,8 @@ XLogChecksums(uint32 new_type)
recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
XLogFlush(recptr);
+
+ elog(LOG, "XLogChecksums %d %X/%X", new_type, LSN_FORMAT_ARGS(recptr));
}
/*
@@ -8862,6 +8968,7 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
+ uint32 data_checksum_version;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In a SHUTDOWN checkpoint, believe the counters exactly */
@@ -8936,6 +9043,10 @@ xlog_redo(XLogReaderState *record)
/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+ elog(LOG, "xlog_redo / ControlFile->data_checksum_version %u => %u",
+ ControlFile->data_checksum_version, checkPoint.dataChecksumState);
+
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
ControlFile->data_checksum_version = checkPoint.dataChecksumState;
@@ -9178,16 +9289,21 @@ xlog_redo(XLogReaderState *record)
{
xl_checkpoint_redo redo_rec;
bool new_state = false;
+ uint32 data_checksum_version;
memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
SetLocalDataChecksumState(redo_rec.data_checksum_version);
if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
new_state = true;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "xlog_redo / XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, redo_rec.data_checksum_version);
+
if (new_state)
EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
}
@@ -9250,18 +9366,27 @@ xlog2_redo(XLogReaderState *record)
if (info == XLOG2_CHECKSUMS)
{
xl_checksum_state state;
+ uint32 data_checksum_version;
memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
SpinLockAcquire(&XLogCtl->info_lck);
+ data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = state.new_checksum_state;
SpinLockRelease(&XLogCtl->info_lck);
+ elog(LOG, "xlog2_redo / XLogCtl->data_checksum_version %u => %u",
+ data_checksum_version, state.new_checksum_state);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+ data_checksum_version = ControlFile->data_checksum_version;
ControlFile->data_checksum_version = state.new_checksum_state;
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ elog(LOG, "xlog2_redo / ControlFile->data_checksum_version %u => %u",
+ data_checksum_version, state.new_checksum_state);
+
/*
* Block on a procsignalbarrier to await all processes having seen the
* change to checksum status. Once the barrier has been passed we can
--
2.54.0
[text/x-patch] 0003-debug-checksums-injection-points.patch (9.6K, ../../[email protected]/4-0003-debug-checksums-injection-points.patch)
download | inline diff:
From 1e58b18a70502339c5608ba352f6e351f6114359 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:14:18 +0200
Subject: [PATCH 03/13] debug: checksums injection points
---
src/backend/access/transam/xlog.c | 74 ++++++++++++++++++++++++++++++-
1 file changed, 72 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 72110d92c8b..8dd003d25e2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4759,6 +4759,13 @@ SetDataChecksumsOnInProgress(void)
elog(LOG, "SetDataChecksumsOnInProgress / start");
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-start", NULL);
+
+ /* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlog");
+ INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlogctl");
+ INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-controlfile");
+
/*
* The state transition is performed in a critical section with
* checkpoints held off to provide crash safety.
@@ -4768,6 +4775,8 @@ SetDataChecksumsOnInProgress(void)
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
+ INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL);
+
SpinLockAcquire(&XLogCtl->info_lck);
data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
@@ -4776,6 +4785,8 @@ SetDataChecksumsOnInProgress(void)
elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
+ INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version %u => %u",
@@ -4785,15 +4796,21 @@ SetDataChecksumsOnInProgress(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOnInProgress / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-before-barrier-wait", NULL);
+
elog(LOG, "SetDataChecksumsOnInProgress / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
WaitForProcSignalBarrier(barrier);
+ INJECTION_POINT("datachecksums-enable-inprogress-checksums-end", NULL);
+
elog(LOG, "SetDataChecksumsOnInProgress / end");
}
@@ -4846,12 +4863,20 @@ SetDataChecksumsOn(void)
SpinLockRelease(&XLogCtl->info_lck);
- INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+ INJECTION_POINT("datachecksums-enable-checksums-start", NULL);
+
+ /* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlog");
+ INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlogctl");
+ INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-controlfile");
+
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+ INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL);
+
SpinLockAcquire(&XLogCtl->info_lck);
data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
@@ -4860,6 +4885,8 @@ SetDataChecksumsOn(void)
elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_VERSION);
+ INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlogctl", NULL);
+
/*
* Update the controlfile before waiting since if we have an immediate
* shutdown while waiting we want to come back up with checksums enabled.
@@ -4873,18 +4900,26 @@ SetDataChecksumsOn(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOn / EmitProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-enable-checksums-before-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-enable-checksums-before-barrier-wait", NULL);
+
elog(LOG, "SetDataChecksumsOn / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
WaitForProcSignalBarrier(barrier);
+ INJECTION_POINT("datachecksums-enable-checksums-end", NULL);
+
elog(LOG, "SetDataChecksumsOn / end");
}
@@ -4930,11 +4965,20 @@ SetDataChecksumsOff(void)
{
SpinLockRelease(&XLogCtl->info_lck);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-start", NULL);
+
+ /* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlog");
+ INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlogctl");
+ INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-controlfile");
+
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
+ INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL);
+
SpinLockAcquire(&XLogCtl->info_lck);
data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
@@ -4943,6 +4987,8 @@ SetDataChecksumsOff(void)
elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
+ INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -4952,15 +4998,21 @@ SetDataChecksumsOff(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-before-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-disable-inprogress-checksums-before-barrier-wait", NULL);
+
elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
WaitForProcSignalBarrier(barrier);
@@ -4979,12 +5031,21 @@ SetDataChecksumsOff(void)
SpinLockRelease(&XLogCtl->info_lck);
}
+ INJECTION_POINT("datachecksums-disable-checksums-start", NULL);
+
+ /* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlog");
+ INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlogctl");
+ INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-controlfile");
+
START_CRIT_SECTION();
/* Ensure that we don't incur a checkpoint during disabling checksums */
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
XLogChecksums(PG_DATA_CHECKSUM_OFF);
+ INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL);
+
SpinLockAcquire(&XLogCtl->info_lck);
data_checksum_version = XLogCtl->data_checksum_version;
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
@@ -4993,6 +5054,8 @@ SetDataChecksumsOff(void)
elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_OFF);
+ INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlogctl", NULL);
+
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -5002,18 +5065,26 @@ SetDataChecksumsOff(void)
UpdateControlFile();
LWLockRelease(ControlFileLock);
+ INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-controlfile", NULL);
+
elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
+ INJECTION_POINT("datachecksums-disable-checksums-before-checkpoint", NULL);
+
elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+ INJECTION_POINT("datachecksums-disable-checksums-before-barrier-wait", NULL);
+
elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
WaitForProcSignalBarrier(barrier);
+ INJECTION_POINT("datachecksums-disable-checksums-end", NULL);
+
elog(LOG, "SetDataChecksumsOff / end");
}
@@ -8968,7 +9039,6 @@ xlog_redo(XLogReaderState *record)
{
CheckPoint checkPoint;
TimeLineID replayTLI;
- uint32 data_checksum_version;
memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
/* In a SHUTDOWN checkpoint, believe the counters exactly */
--
2.54.0
[text/x-patch] 0004-debug-checkpointer-injection-points.patch (6.0K, ../../[email protected]/5-0004-debug-checkpointer-injection-points.patch)
download | inline diff:
From 0c3dd8ac1df7fd1686374555eb766e4517c4c1a8 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:15:52 +0200
Subject: [PATCH 04/13] debug: checkpointer injection points
---
src/backend/access/transam/xlog.c | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8dd003d25e2..7c4e412093c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4762,6 +4762,7 @@ SetDataChecksumsOnInProgress(void)
INJECTION_POINT("datachecksums-enable-inprogress-checksums-start", NULL);
/* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-before-xlog");
INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlog");
INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlogctl");
INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-controlfile");
@@ -4773,6 +4774,8 @@ SetDataChecksumsOnInProgress(void)
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+ INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-before-xlog", NULL);
+
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL);
@@ -4866,6 +4869,7 @@ SetDataChecksumsOn(void)
INJECTION_POINT("datachecksums-enable-checksums-start", NULL);
/* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-enable-checksums-before-xlog");
INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlog");
INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlogctl");
INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-controlfile");
@@ -4873,6 +4877,8 @@ SetDataChecksumsOn(void)
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+ INJECTION_POINT_CACHED("datachecksums-enable-checksums-before-xlog", NULL);
+
XLogChecksums(PG_DATA_CHECKSUM_VERSION);
INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL);
@@ -4968,6 +4974,7 @@ SetDataChecksumsOff(void)
INJECTION_POINT("datachecksums-disable-inprogress-checksums-start", NULL);
/* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-before-xlog");
INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlog");
INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlogctl");
INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-controlfile");
@@ -4975,6 +4982,8 @@ SetDataChecksumsOff(void)
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+ INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-before-xlog", NULL);
+
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL);
@@ -5034,6 +5043,7 @@ SetDataChecksumsOff(void)
INJECTION_POINT("datachecksums-disable-checksums-start", NULL);
/* load before critical section */
+ INJECTION_POINT_LOAD("datachecksums-disable-checksums-before-xlog");
INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlog");
INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlogctl");
INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-controlfile");
@@ -5042,6 +5052,8 @@ SetDataChecksumsOff(void)
/* Ensure that we don't incur a checkpoint during disabling checksums */
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+ INJECTION_POINT_CACHED("datachecksums-disable-checksums-before-xlog", NULL);
+
XLogChecksums(PG_DATA_CHECKSUM_OFF);
INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL);
@@ -7611,6 +7623,12 @@ CreateCheckPoint(int flags)
/* Run these points outside the critical section. */
INJECTION_POINT("create-checkpoint-initial", NULL);
INJECTION_POINT_LOAD("create-checkpoint-run");
+ INJECTION_POINT_LOAD("checkpoint-before-redo");
+ INJECTION_POINT_LOAD("checkpoint-before-xlogctl-checksums");
+ INJECTION_POINT_LOAD("checkpoint-after-xlogctl-checksums");
+ INJECTION_POINT_LOAD("checkpoint-before-redo-checksums");
+ INJECTION_POINT_LOAD("checkpoint-before-redo-wal");
+ INJECTION_POINT_LOAD("checkpoint-after-redo-wal");
/*
* Use a critical section to force system panic if we have trouble.
@@ -7676,6 +7694,8 @@ CreateCheckPoint(int flags)
else
checkPoint.PrevTimeLineID = checkPoint.ThisTimeLineID;
+ INJECTION_POINT_CACHED("checkpoint-before-redo", NULL);
+
/*
* We must block concurrent insertions while examining insert state.
*/
@@ -7684,6 +7704,8 @@ CreateCheckPoint(int flags)
checkPoint.fullPageWrites = Insert->fullPageWrites;
checkPoint.wal_level = wal_level;
+ INJECTION_POINT_CACHED("checkpoint-before-xlogctl-checksums", NULL);
+
/*
* Get the current data_checksum_version value from xlogctl, valid at the
* time of the checkpoint.
@@ -7692,6 +7714,8 @@ CreateCheckPoint(int flags)
checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
SpinLockRelease(&XLogCtl->info_lck);
+ INJECTION_POINT_CACHED("checkpoint-after-xlogctl-checksums", NULL);
+
if (shutdown)
{
XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7749,6 +7773,8 @@ CreateCheckPoint(int flags)
{
xl_checkpoint_redo redo_rec;
+ INJECTION_POINT_CACHED("checkpoint-before-redo-checksums", NULL);
+
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
SpinLockAcquire(&XLogCtl->info_lck);
@@ -7756,6 +7782,8 @@ CreateCheckPoint(int flags)
SpinLockRelease(&XLogCtl->info_lck);
WALInsertLockRelease();
+ INJECTION_POINT_CACHED("checkpoint-before-redo-wal", NULL);
+
/* Include WAL level in record for WAL summarizer's benefit. */
XLogBeginInsert();
XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
@@ -7768,6 +7796,8 @@ CreateCheckPoint(int flags)
* checkpoint is complete.
*/
checkPoint.redo = RedoRecPtr;
+
+ INJECTION_POINT_CACHED("checkpoint-after-redo-wal", NULL);
}
/* Update the info_lck-protected copy of RedoRecPtr as well */
--
2.54.0
[text/x-patch] 0005-TAP-10-concurrent-checksum-changes.patch (16.0K, ../../[email protected]/6-0005-TAP-10-concurrent-checksum-changes.patch)
download | inline diff:
From 24ab81ae666ed9de683091d6feecbf80b2dd5bb1 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:14:49 +0200
Subject: [PATCH 05/13] TAP 10: concurrent checksum changes
---
.../t/010_concurrent_changes.pl | 296 ++++++++++++++++++
1 file changed, 296 insertions(+)
create mode 100644 src/test/modules/test_checksums/t/010_concurrent_changes.pl
diff --git a/src/test/modules/test_checksums/t/010_concurrent_changes.pl b/src/test/modules/test_checksums/t/010_concurrent_changes.pl
new file mode 100644
index 00000000000..1b17b496033
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_concurrent_changes.pl
@@ -0,0 +1,296 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+ my ($node, $point) = @_;
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+}
+
+sub wait_injection_point
+{
+ my ($node, $point) = @_;
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+}
+
+sub wakeup_injection_point
+{
+ my ($node, $point) = @_;
+
+ note('waking the init injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');"
+ );
+
+ note("detaching the init injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+}
+
+# Test behavior with two concurrent checksum transitions.
+#
+# The cluster is initialized into checksum state $start (enabled/disabled),
+# and two changes are initiated in a controled way. The $first change gets
+# paused at a selected injection point $point, at which point $second change
+# gets initiated. Once the checksum resolves the cluster state, the state
+# is compared to the expected $final state.
+#
+# arguments:
+#
+# - start - starting checksum state (enabled/disabled)
+# - first - first checksum change
+# - second - second checksum change
+# - init - initial injection point (see comment later)
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The injection point is triggered only by the datachecksum launcher, and
+# there can be only one such process. So there's no risk of hitting the
+# injection point by both changes.
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX The first change gets "unpaused" after the second checksum change
+# gets initiated, but maybe that's not a sufficiently deterministic?
+#
+# XXX Should we detach the injection point $point before initiating the
+# second change? I'm not sure if there's a small race condition because
+# of waking up and detaching the injection point.
+sub test_checksum_transition
+{
+ my ($start, $first, $second, $init, $point, $final) = @_;
+
+ # print the current test instructions, both into TAP output and into
+ # the server log, to make correlation easier
+
+ my $initstr = 'undef';
+ $initstr = $init if defined($init);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $first . " / " . $second . " / " . $initstr . " / " . $point . " / " . $final . " =========='");
+
+ note($start . " / " . $first . " / " . $second . " / " . $initstr . " / " . $point . " / " . $final);
+
+ # put the cluster into the initial checksum state, synchronously
+ note('changing checksums into initial state: ' . $start);
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ # Wait on an injection point outside a critical section, to initialize
+ # the shmem (which can't be done in critical section).
+ attach_injection_point($node, $init) if defined($init);
+
+ # Wait on the injection point, to pause the first checksum change.
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+
+ # Trigger the checksum change, asynchronously
+ note("triggering first checksum change: " . $first);
+ enable_data_checksums($node) if ($first eq 'enable');
+ disable_data_checksums($node) if ($first eq 'disable');
+
+ # Handle the initial injection point - wait, wakeup and detatch. This
+ # initializes the shmem for the 'wait' action.
+ wait_injection_point($node, $init) if defined($init);
+ wakeup_injection_point($node, $init) if defined($init);
+
+ # Wait for the actual injection point to be hit by the state change.
+ wait_injection_point($node, $point);
+
+ # The first checksum state change is waitinig on the injection point.
+ # Trigger the concurrent change (also asynchronously).
+ note("triggering second checksum change: " . $second);
+ enable_data_checksums($node) if ($second eq 'enable');
+ disable_data_checksums($node) if ($second eq 'disable');
+
+ # Wake the injection point, so that the first change can proceed.
+ wakeup_injection_point($node, $point);
+
+ # Wait until there are no ongoing checksum changes, which we determine
+ # by looking for a checksum launcher process.
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+
+ # Does the final checksum state match the expected state?
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# concurrent enable + disable, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-start', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-end', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-start', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-end', 'off');
+
+# concurrent enable + enable, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-end', 'on');
+
+# concurrent disable + disable, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-end', 'off');
+
+# concurrent disable + enable, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-inprogress-checksums-start', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-start', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-end', 'on');
+
+$node->stop;
+done_testing();
--
2.54.0
[text/x-patch] 0006-TAP-11-concurrency-with-checkpoints.patch (12.2K, ../../[email protected]/7-0006-TAP-11-concurrency-with-checkpoints.patch)
download | inline diff:
From ae1ef6cd0a2c4d2fcddb34db282c55a7f2fe509c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Wed, 15 Apr 2026 22:22:48 +0200
Subject: [PATCH 06/13] TAP 11: concurrency with checkpoints
---
.../t/011_concurrent_checkpoint.pl | 276 ++++++++++++++++++
1 file changed, 276 insertions(+)
create mode 100644 src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
diff --git a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
new file mode 100644
index 00000000000..4cea74914d4
--- /dev/null
+++ b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
@@ -0,0 +1,276 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+ my ($node, $point) = @_;
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+}
+
+sub wait_injection_point
+{
+ my ($node, $point) = @_;
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+}
+
+sub wakeup_injection_point
+{
+ my ($node, $point) = @_;
+
+ note('waking the init injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');"
+ );
+
+ note("detaching the init injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint.
+#
+# The cluster is initialized into checksum state $start (enabled/disabled),
+# and a checksum change is initiated in a controled way. It gets paused at
+# a selected injection point $point, and a checkpoint is performed. Then
+# the injection point is waken up. Once the checksum change completes, the
+# checksum state is compared to the expected $final state.
+#
+# The checkpoint is performed synchronously, while the checksum worker
+# is paused on the injection point.
+#
+# Finally, after the checksum change completes, the instance is restarted
+# (in either fast ot immediate mode, picked randomly), and the final
+# checksum state is validated against the expected value. The server log
+# is checked for checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - start - starting checksum state (enabled/disabled)
+# - change - checksum change
+# - init - initial injection point (see comment later)
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+sub test_checksum_transition
+{
+ my ($start, $change, $init, $point, $final) = @_;
+
+ # print the current test instructions, both into TAP output and into
+ # the server log, to make correlation easier
+
+ my $initstr = 'undef';
+ $initstr = $init if defined($init);
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / " . $initstr . " / " . $point . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / " . $initstr . " / " . $point . " / " . $final);
+
+ # put the cluster into the initial checksum state, synchronously
+ note('changing checksums into initial state: ' . $start);
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ # Wait on an injection point outside a critical section, to initialize
+ # the shmem (which can't be done in critical section).
+ attach_injection_point($node, $init) if defined($init);
+
+ # Wait on the injection point, to pause the checksum change.
+ attach_injection_point($node, $point);
+
+ # Trigger the checksum change, asynchronously
+ note("triggering checksum change: " . $change);
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ # Handle the initial injection point - wait, wakeup and detatch. This
+ # initializes the shmem for the 'wait' action.
+ wait_injection_point($node, $init) if defined($init);
+ wakeup_injection_point($node, $init) if defined($init);
+
+ # Wait for the injection point to be hit by the state change.
+ wait_injection_point($node, $point);
+
+ # The checksum state change is paused on the injection point. Perform
+ # the checkpoint (synchronously).
+ note('checkpoint');
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ # Wake the injection point, so that the first change can proceed.
+ wakeup_injection_point($node, $point);
+
+ # Wait until there are no ongoing checksum changes, which we determine
+ # by looking for a checksum launcher process.
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+
+ # Does the final checksum state match the expected state?
+ test_checksum_state($node, $final);
+
+ # restart the cluster, possibly in immediate mode, to simulate a crash
+ #
+ # XXX maybe we should do just immediate shutdowns, because the shutdown
+ # checkpoint will overwrite the state state with "current" one, not the
+ # state produced by the concurrent change + checkpoint.
+ $node->stop(stopmode());
+ $node->start;
+
+ # Does the final checksum state match the expected state?
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+# concurrent enable + checkpoint, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-end', 'on');
+
+# concurrent disable + checkpoint, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-end', 'off');
+
+$node->stop;
+done_testing();
--
2.54.0
[text/x-patch] 0007-TAP-12-crashes-at-injection-points.patch (13.4K, ../../[email protected]/8-0007-TAP-12-crashes-at-injection-points.patch)
download | inline diff:
From 57a5e353dbfcaaac1fa7cd25dc38f3e1807ccafa Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Wed, 15 Apr 2026 23:06:14 +0200
Subject: [PATCH 07/13] TAP 12: crashes at injection points
---
.../t/012_concurrent_checkpoint_crash.pl | 282 ++++++++++++++++++
1 file changed, 282 insertions(+)
create mode 100644 src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
diff --git a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
new file mode 100644
index 00000000000..de169f47034
--- /dev/null
+++ b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
@@ -0,0 +1,282 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+ my ($node, $point) = @_;
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+}
+
+sub wait_injection_point
+{
+ my ($node, $point) = @_;
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+}
+
+sub wakeup_injection_point
+{
+ my ($node, $point) = @_;
+
+ note('waking the init injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');"
+ );
+
+ note("detaching the init injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint,
+# followed by a crash.
+#
+# The test puts the instance into the initial checksum state $start,
+# triggers a checksum change that pauses on a first injection point. Then
+# a checkpoint is performed, and the checksum change proceeds either to
+# a second injection point or finishes.
+#
+# Then the instance get restarted in immediate mode to simulate failure,
+# and the final checksum state (after recovery) is validated against the
+# expected value. The server log is checked for checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - start - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point1 - injection point before checkpoint
+# - point2 - injection point after checkpoint
+# - final - expected checksum state at the end
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+sub test_checksum_transition
+{
+ my ($start, $change, $init, $point1, $point2, $final) = @_;
+
+ my $initstr = 'undef';
+ my $point2str = 'undef';
+
+ # print the current test instructions, both into TAP output and into
+ # the server log, to make correlation easier
+
+ $initstr = $init if defined($init);
+ $point2str = $point2 if defined($point2);
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / " . $initstr . " / " . $point1 . " / " . $point2str . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / " . $initstr . " / " . $point1 . " / " . $point2str . " / " . $final);
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ # put the cluster into the initial checksum state, synchronously
+ note('changing checksums into initial state: ' . $start);
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ # Wait on an injection point outside a critical section, to initialize
+ # the shmem (which can't be done in critical section).
+ attach_injection_point($node, $init) if defined($init);
+
+ # Wait on the two injection points, to pause the checksum change.
+ attach_injection_point($node, $point1);
+
+ # The second injection point is optional, so only attach it then.
+ attach_injection_point($node, $point2) if defined($point2);
+
+ # Trigger the checksum change, asynchronously
+ note("triggering checksum change: " . $change);
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ # Handle the initial injection point - wait, wakeup and detatch. This
+ # initializes the shmem for the 'wait' action.
+ wait_injection_point($node, $init) if defined($init);
+ wakeup_injection_point($node, $init) if defined($init);
+
+ # Wait for the first injection point to be hit by the state change.
+ wait_injection_point($node, $point1);
+
+ # The checksum state change is paused on the first injection point.
+ # Perform the checkpoint (synchronously).
+ note('checkpoint');
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ # Wake the injection point, so that the first change can proceed.
+ wakeup_injection_point($node, $point1);
+
+ # Either wait for the second injection point - if defined, or for the
+ # checksum change to complete.
+
+ if (defined($point2))
+ {
+ wait_injection_point($node, $point2);
+ }
+ else
+ {
+ # Wait until there are no ongoing checksum changes, which we determine
+ # by looking for a checksum launcher process.
+ note('wait for the checksum launcher to exit');
+ $node->poll_query_until('postgres',
+ "SELECT count(*) = 0 "
+ . "FROM pg_catalog.pg_stat_activity "
+ . "WHERE backend_type = 'datachecksum launcher';");
+ note('checksum launcher exited');
+ }
+
+ # restart the cluster, in immediate mode, to simulate a crash
+ $node->stop('immediate');
+ $node->start;
+
+ # Does the final checksum state match the expected state?
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+# concurrent enable + checkpoint, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'datachecksums-enable-inprogress-checksums-end', 'off');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-before-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'datachecksums-enable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'datachecksums-enable-checksums-end', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-end', undef, 'on');
+
+# concurrent disable + checkpoint, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'datachecksums-disable-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-before-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'datachecksums-disable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'datachecksums-disable-checksums-end', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-end', undef, 'off');
+
+$node->stop;
+done_testing();
--
2.54.0
[text/x-patch] 0008-TAP-13-concurrency-with-checkpoint-REDO.patch (23.8K, ../../[email protected]/9-0008-TAP-13-concurrency-with-checkpoint-REDO.patch)
download | inline diff:
From ddaca81f530a371a2dfab67ad9da49ef1f8aa69e Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:16:00 +0200
Subject: [PATCH 08/13] TAP 13: concurrency with checkpoint REDO
---
.../t/013_async_checkpoint_crash.pl | 604 ++++++++++++++++++
1 file changed, 604 insertions(+)
create mode 100644 src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
diff --git a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
new file mode 100644
index 00000000000..4a310506482
--- /dev/null
+++ b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
@@ -0,0 +1,604 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+
+# make sure we don't hit checkpoints unless desired
+$node->append_conf(
+ 'postgresql.conf',
+ qq[
+checkpoint_timeout = 1h
+max_wal_size = 32GB
+]);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a checkpoint in the background against the server specified via the
+# port passed as parameter.
+sub background_checkpoint
+{
+ my $port = shift;
+
+ my @cmd = ('psql', '-p', $port, '-c', 'checkpoint', 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+ my ($node, $point) = @_;
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+}
+
+sub wait_injection_point
+{
+ my ($node, $point) = @_;
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+}
+
+sub wakeup_injection_point
+{
+ my ($node, $point) = @_;
+
+ # detach before wakeup, so that we can't hit it right away
+ note("detaching the init injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+
+ note('waking the init injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');"
+ );
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint,
+# followed by a crash.
+#
+# The test puts the instance into the initial checksum state $start,
+# initiates a checksum change and waits for it to pause on the $init
+# injection point. Then a checkpoint is started (in the background), and
+# the two processes go through a list of injection points to interleave
+# them in a particular way.
+#
+# After reaching a $stop injection point, the cluster get restarted in an
+# immediate mode to simulate failure. The checksum state (after recovery)
+# is validated against the expected value. The server log is checked for
+# checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - start - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - init - initial injection point to wait on
+# - stop - injection point to wait on before restarting
+# - final - expected checksum state at the end
+# - steps - a list of injection points to wait on / unpause
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we first need to pause on an injection point outside
+# a critical section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+#
+# XXX The steps are not entirely deterministic, because when unpausing
+# an injection point, we don't know if the other process is already
+# paused on the following point.
+#
+sub test_checksum_sequence
+{
+ my ($start, $change, $init, $stop, $final, @steps) = @_;
+
+ # print the current test instructions, both into TAP output and into
+ # the server log, to make correlation easier
+
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $start . " / " . $change . " / (" . @steps . ") / " . $init . " / " . $stop . " / " . $final . " =========='");
+
+ note($start . " / " . $change . " / (" . @steps . ") / " . $init . " / " . $stop . " / " . $final);
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ # put the cluster into the initial checksum state, synchronously
+ note('changing checksums into initial state: ' . $start);
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ # attach the initial injection point
+ attach_injection_point($node, $init);
+
+ # when we're at it, attach the stop injection point
+ attach_injection_point($node, $stop);
+
+ # Do a checkpoint first, so that we don't accidentaly start the next
+ # checkpoint at an unecpected time. We want the next checkpoint to be
+ # the background one.
+ $node->safe_psql('postgres', "CHECKPOINT");
+
+ # Trigger the checksum change, asynchronously
+ note("triggering checksum change: " . $change);
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ # wait for the initial injection point
+ wait_injection_point($node, $init);
+
+ # now attach all the regular injection points
+ my $n = @steps;
+ for ($a = 0; $a < $n; $a++)
+ {
+ my $point = $steps[$a];
+ attach_injection_point($node, $point);
+ }
+
+ # initiate a background checkpoint
+ background_checkpoint($node->port);
+
+ # wakeup the initial injection point, to start the main part
+ wakeup_injection_point($node, $init);
+
+ # wait for regular injection points in sequence, wake and detach them
+ for ($a = 0; $a < $n; $a++)
+ {
+ my $point = $steps[$a];
+
+ wait_injection_point($node, $point);
+ wakeup_injection_point($node, $point);
+ }
+
+ # wait for the stop injection point
+ wait_injection_point($node, $stop);
+
+ # restart the cluster, in immediate mode, to simulate a crash
+ $node->stop('immediate');
+ $node->start;
+
+ # Does the final checksum state match the expected state?
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+my @steps = undef;
+
+## checksums INPROGRESS ON
+
+note('TEST INPROGRESS-ON/1');
+@steps = qw(datachecksums-enable-inprogress-checksums-before-xlog
+ datachecksums-enable-inprogress-checksums-after-xlog
+ datachecksums-enable-inprogress-checksums-after-xlogctl
+ datachecksums-enable-inprogress-checksums-after-controlfile
+ create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/2');
+@steps = qw(create-checkpoint-initial
+ datachecksums-enable-inprogress-checksums-before-xlog
+ datachecksums-enable-inprogress-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ datachecksums-enable-inprogress-checksums-after-xlogctl
+ datachecksums-enable-inprogress-checksums-after-controlfile
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/3');
+@steps = qw(create-checkpoint-initial
+ datachecksums-enable-inprogress-checksums-before-xlog
+ datachecksums-enable-inprogress-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ datachecksums-enable-inprogress-checksums-after-xlogctl
+ datachecksums-enable-inprogress-checksums-after-controlfile
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/4');
+@steps = qw(create-checkpoint-initial
+ datachecksums-enable-inprogress-checksums-before-xlog
+ datachecksums-enable-inprogress-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ datachecksums-enable-inprogress-checksums-after-xlogctl
+ datachecksums-enable-inprogress-checksums-after-controlfile
+ checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/5');
+@steps = qw(create-checkpoint-initial
+ datachecksums-enable-inprogress-checksums-before-xlog
+ datachecksums-enable-inprogress-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-enable-inprogress-checksums-after-xlogctl
+ datachecksums-enable-inprogress-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/6');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ datachecksums-enable-inprogress-checksums-before-xlog
+ datachecksums-enable-inprogress-checksums-after-xlog
+ datachecksums-enable-inprogress-checksums-after-xlogctl
+ datachecksums-enable-inprogress-checksums-after-controlfile
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+## checksums ON
+
+note('TEST ON/1');
+@steps = qw(datachecksums-enable-checksums-before-xlog
+ datachecksums-enable-checksums-after-xlog
+ datachecksums-enable-checksums-after-xlogctl
+ datachecksums-enable-checksums-after-controlfile
+ create-checkpoint-initial
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+note('TEST ON/2');
+@steps = qw(create-checkpoint-initial
+ datachecksums-enable-checksums-before-xlog
+ datachecksums-enable-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-enable-checksums-after-xlogctl
+ datachecksums-enable-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+note('TEST ON/3');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ datachecksums-enable-checksums-before-xlog
+ datachecksums-enable-checksums-after-xlog
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-enable-checksums-after-xlogctl
+ datachecksums-enable-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+note('TEST ON/4');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ datachecksums-enable-checksums-before-xlog
+ datachecksums-enable-checksums-after-xlog
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-enable-checksums-after-xlogctl
+ datachecksums-enable-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+
+## checksums INPROGRESS OFF
+
+note('TEST INPROGRESS-OFF/1');
+@steps = qw(datachecksums-disable-inprogress-checksums-before-xlog
+ datachecksums-disable-inprogress-checksums-after-xlog
+ datachecksums-disable-inprogress-checksums-after-xlogctl
+ datachecksums-disable-inprogress-checksums-after-controlfile
+ datachecksums-disable-inprogress-checksums-before-checkpoint
+ create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+note('TEST INPROGRESS-OFF/2');
+@steps = qw(create-checkpoint-initial
+ datachecksums-disable-inprogress-checksums-before-xlog
+ datachecksums-disable-inprogress-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-inprogress-checksums-after-xlogctl
+ datachecksums-disable-inprogress-checksums-after-controlfile
+ datachecksums-disable-inprogress-checksums-before-checkpoint
+ datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+note('TEST INPROGRESS-OFF/3');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ datachecksums-disable-inprogress-checksums-before-xlog
+ datachecksums-disable-inprogress-checksums-after-xlog
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-inprogress-checksums-after-xlogctl
+ datachecksums-disable-inprogress-checksums-after-controlfile
+ datachecksums-disable-inprogress-checksums-before-checkpoint
+ datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+note('TEST INPROGRESS-OFF/4');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ datachecksums-disable-inprogress-checksums-before-xlog
+ datachecksums-disable-inprogress-checksums-after-xlog
+ datachecksums-disable-inprogress-checksums-after-xlogctl
+ datachecksums-disable-inprogress-checksums-after-controlfile
+ datachecksums-disable-inprogress-checksums-before-checkpoint
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+## checksums OFF
+
+note('TEST OFF/1');
+@steps = qw(datachecksums-disable-checksums-before-xlog
+ datachecksums-disable-checksums-after-xlog
+ datachecksums-disable-checksums-after-xlogctl
+ datachecksums-disable-checksums-after-controlfile
+ datachecksums-disable-checksums-before-checkpoint
+ create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/2');
+@steps = qw(create-checkpoint-initial
+ datachecksums-disable-checksums-before-xlog
+ datachecksums-disable-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ datachecksums-disable-checksums-after-xlogctl
+ datachecksums-disable-checksums-after-controlfile
+ datachecksums-disable-checksums-before-checkpoint
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/3');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ datachecksums-disable-checksums-before-xlog
+ datachecksums-disable-checksums-after-xlog
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ datachecksums-disable-checksums-after-xlogctl
+ datachecksums-disable-checksums-after-controlfile
+ datachecksums-disable-checksums-before-checkpoint
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/4');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ datachecksums-disable-checksums-before-xlog
+ datachecksums-disable-checksums-after-xlog
+ datachecksums-disable-checksums-after-xlogctl
+ datachecksums-disable-checksums-after-controlfile
+ datachecksums-disable-checksums-before-checkpoint
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/5');
+@steps = qw(create-checkpoint-initial
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ checkpoint-before-redo-checksums
+ datachecksums-disable-checksums-before-xlog
+ datachecksums-disable-checksums-after-xlog
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ datachecksums-disable-checksums-after-xlogctl
+ datachecksums-disable-checksums-after-controlfile
+ datachecksums-disable-checksums-before-checkpoint
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/6');
+@steps = qw(create-checkpoint-initial
+ datachecksums-disable-checksums-before-xlog
+ datachecksums-disable-checksums-after-xlog
+ checkpoint-before-redo
+ checkpoint-before-xlogctl-checksums
+ checkpoint-after-xlogctl-checksums
+ datachecksums-disable-checksums-after-xlogctl
+ checkpoint-before-redo-checksums
+ checkpoint-before-redo-wal
+ checkpoint-after-redo-wal
+ datachecksums-disable-checksums-after-controlfile
+ datachecksums-disable-checksums-before-checkpoint
+ checkpoint-before-old-wal-removal
+ datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+$node->stop;
+done_testing();
--
2.54.0
[text/x-patch] 0009-TAP-14-checkpoints-with-crashes.patch (12.6K, ../../[email protected]/10-0009-TAP-14-checkpoints-with-crashes.patch)
download | inline diff:
From ff3cd3e661d508db102e18752fee3335fb4ceaca Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 13:35:56 +0200
Subject: [PATCH 09/13] TAP 14: checkpoints with crashes
This shows the non-determinism / race condition with the step in test
012_crashes, thanks to better sequencing of the two sides (checksum
worker + checkpointer). The related 012 test is this:
test_checksum_transition('disabled', 'enable',
'datachecksums-enable-checksums-start',
'datachecksums-enable-checksums-after-xlog',
'datachecksums-enable-checksums-after-xlogctl',
'off');
---
...ncurrent_checkpoint_crash_deterministic.pl | 359 ++++++++++++++++++
1 file changed, 359 insertions(+)
create mode 100644 src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl
diff --git a/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl b/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl
new file mode 100644
index 00000000000..bfca3983965
--- /dev/null
+++ b/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl
@@ -0,0 +1,359 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# This test suite is expensive, or very expensive, to execute. There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite. The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+ $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+ plan skip_all => 'Expensive data checksums test disabled'
+ unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+ [
+ 'pgbench', '-p', $node->port, '-i',
+ '-s', $scalefactor, '-q', 'postgres'
+ ]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+ my $port = shift;
+
+ # If a previous pgbench is still running, start by shutting it down.
+ $pgbench->finish if $pgbench;
+
+ my $clients = 1;
+ my $runtime = 2;
+
+ if ($extended)
+ {
+ # Randomize the number of pgbench clients a bit (range 1-16)
+ $clients = 1 + int(rand(15));
+ $runtime = 600;
+ }
+ my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+ # Randomize whether we spawn connections or not
+ push(@cmd, '-C') if ($extended && cointoss);
+ # Finally add the database name to use
+ push(@cmd, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Run a SQL in the background against the server specified via the
+# port passed as parameter.
+sub background_sql
+{
+ my ($port, $sql) = @_;
+
+ my @cmd = ('psql', '-p', $port, '-c', $sql, 'postgres');
+
+ $pgbench = IPC::Run::start(
+ \@cmd,
+ '<' => '/dev/null',
+ '>' => '/dev/null',
+ '2>' => '/dev/null',
+ IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+ my ($node, $point) = @_;
+ note('attaching injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_attach('" . $point . "','wait');"
+ );
+}
+
+sub wait_injection_point
+{
+ my ($node, $point) = @_;
+
+ note("waiting for the injection point to be hit");
+ $node->poll_query_until(
+ 'postgres',
+ "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+ '1');
+}
+
+sub wakeup_injection_point
+{
+ my ($node, $point) = @_;
+
+ # detach before wakeup, so that we can't hit it right away
+ note("detaching the init injection point");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('" . $point . "');");
+
+ note('waking the init injection point: ' . $point);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('" . $point . "');"
+ );
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint,
+# followed by a crash.
+#
+# The test puts the instance into the initial checksum state $start,
+# triggers a checksum change. A concurrent checkpoint is performed, and
+# and the test steps through the checksum change and the checkpoint in
+# a deterministic way.
+#
+# Then the instance get restarted in immediate mode to simulate failure,
+# and the final checksum state (after recovery) is validated against the
+# expected value. The server log is checked for checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - name - name of the test sequence (for easy identification)
+# - start - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - final - expected checksum state at the end
+# - steps - steps to go through (wait, wakeup, sql)
+#
+# XXX This is similar to 012_concurrent_checkpoint_crash, except that the
+# checkpoint happens asynchronously (in the background), and the steps
+# step through the injection points in a deterministic way.
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'. This needs to
+# be done for individual processes (e.g. the checkpointer needs to init a
+# 'wait' point too, it's not enough to init one in the checksum worker).
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+sub test_checksum_sequence
+{
+ my ($name, $start, $change, $final, @steps) = @_;
+
+ # Start the test suite with pgbench running.
+ background_rw_pgbench($node->port);
+
+ # print the current test instructions, both into TAP output and into
+ # the server log, to make correlation easier
+
+ note($name);
+ $node->safe_psql('postgres',
+ "SELECT '========== " . $name . " =========='");
+
+ # put the cluster into the initial checksum state, synchronously
+ note('changing checksums into initial state: ' . $start);
+ enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+ disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+ # attach all the injection points mentioned in 'wait' steps
+ my $n = @steps;
+ my $s;
+ note('processing ' . $n . ' steps');
+ for ($s = 0; $s < $n; $s++)
+ {
+ my @step = $steps[$s];
+ my $action = $steps[$s][0];
+ my $value = $steps[$s][1];
+
+ note('step ' . $s . ' action ' . $action . ' / ' . $value);
+ if ($action eq 'wait')
+ {
+ attach_injection_point($node, $value);
+ }
+ }
+
+ # Trigger the checksum change, asynchronously
+ note("triggering checksum change: " . $change);
+ enable_data_checksums($node) if ($change eq 'enable');
+ disable_data_checksums($node) if ($change eq 'disable');
+
+ # now process all the steps - wait, wakeup, sql, etc.
+ $n = @steps;
+ note('processing ' . $n . ' steps');
+ for ($s = 0; $s < $n; $s++)
+ {
+ my @step = $steps[$s];
+ my $action = $steps[$s][0];
+ my $value = $steps[$s][1];
+
+ note('step ' . $s . ' action ' . $action . ' / ' . $value);
+ if ($action eq 'wait')
+ {
+ wait_injection_point($node, $value);
+ }
+ elsif ($action eq 'wakeup')
+ {
+ wakeup_injection_point($node, $value);
+ }
+ elsif ($action eq 'sql')
+ {
+ note('sql: ' . $value);
+
+ # initiate a background sql
+ background_sql($node->port, $value);
+ }
+ }
+
+ # restart the cluster, in immediate mode, to simulate a crash
+ $node->stop('immediate');
+ $node->start;
+
+ # Does the final checksum state match the expected state?
+ test_checksum_state($node, $final);
+
+ # Since the log isn't being written to now, parse the log and check
+ # for instances of checksum verification failures.
+ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+ $node_loglocation);
+ unlike(
+ $log,
+ qr/page verification failed,.+\d$/,
+ "no checksum validation errors in primary log (during WAL recovery)"
+ );
+ $node_loglocation = -s $node->logfile;
+}
+
+# sequence of steps to perform, each step is defined as an array of actions
+#
+# - type of an action (wait, wakeup, sql)
+# - injection point or SQL command
+#
+# after processing all commands, the instance gets killed / shut down with
+# immediate mode
+my @steps = undef;
+
+# CHECKSUMS-ON-1
+#
+# checkpoint after: datachecksums-enable-checksums-after-xlog
+# crash after: datachecksums-enable-checksums-after-xlogctl
+# checkpoint completes
+#
+# steps:
+# 1) checksums: write XLOG2_CHECKSUMS to WAL
+# 2) start checkpoint
+# 3) checkpointer: read XLogCtl->data_checksum_version
+# 4) checksums: update XLogCtl->data_checksum_version
+# 5) checkpointer: write CHECKPOINT_REDO
+# 6) checkpointer: complete the checkpoint
+# 7) crash
+@steps = (
+ ['wait', 'datachecksums-enable-checksums-start'], # initialize the wait
+ ['wakeup', 'datachecksums-enable-checksums-start'],
+ ['wait', 'datachecksums-enable-checksums-after-xlog'],
+ ['sql', 'checkpoint'],
+ ['wait', 'create-checkpoint-initial'], # initialize the wait
+ ['wakeup', 'create-checkpoint-initial'],
+ ['wait', 'checkpoint-before-redo-wal'],
+ ['wakeup', 'datachecksums-enable-checksums-after-xlog'],
+ ['wait', 'datachecksums-enable-checksums-after-xlogctl'],
+ ['wakeup', 'checkpoint-before-redo-wal'],
+ ['wait', 'checkpoint-before-old-wal-removal'],
+ ['wakeup', 'checkpoint-before-old-wal-removal']
+);
+
+test_checksum_sequence('CHECKSUMS-ON-1', 'disabled', 'enable', 'off', @steps);
+
+# CHECKSUMS-ON-2
+#
+# checkpoint after: datachecksums-enable-checksums-after-xlog
+# crash after: datachecksums-enable-checksums-after-xlogctl
+# checkpoint completes
+#
+# steps:
+# 1) checksums: write XLOG2_CHECKSUMS to WAL
+# 2) start checkpoint
+# 3) checksums: update XLogCtl->data_checksum_version
+# 4) checkpointer: read XLogCtl->data_checksum_version
+# 5) checkpointer: write CHECKPOINT_REDO
+# 6) checkpointer: complete the checkpoint
+# 7) crash
+@steps = (
+ ['wait', 'datachecksums-enable-checksums-start'], # initialize the wait
+ ['wakeup', 'datachecksums-enable-checksums-start'],
+ ['wait', 'datachecksums-enable-checksums-after-xlog'],
+ ['sql', 'checkpoint'],
+ ['wait', 'create-checkpoint-initial'], # initialize the wait
+ ['wakeup', 'datachecksums-enable-checksums-after-xlog'],
+ ['wakeup', 'create-checkpoint-initial'],
+ ['wait', 'checkpoint-before-xlogctl-checksums'],
+ ['wait', 'datachecksums-enable-checksums-after-xlogctl'],
+ ['wakeup', 'checkpoint-before-xlogctl-checksums'],
+ ['wait', 'checkpoint-before-old-wal-removal'],
+ ['wakeup', 'checkpoint-before-old-wal-removal']
+);
+
+test_checksum_sequence('CHECKSUMS-ON-2', 'disabled', 'enable', 'off', @steps);
+
+# CHECKSUMS-ON-3
+#
+# checkpoint happens after checksum worker updates XLogCtl
+@steps = (
+ ['wait', 'datachecksums-enable-checksums-start'], # initialize the wait
+ ['wakeup', 'datachecksums-enable-checksums-start'],
+ ['wait', 'datachecksums-enable-checksums-after-xlogctl'],
+ ['sql', 'checkpoint'],
+ ['wait', 'create-checkpoint-initial'], # initialize the wait
+ ['wakeup', 'create-checkpoint-initial'],
+ ['wait', 'checkpoint-before-redo-checksums'],
+ ['wakeup', 'checkpoint-before-redo-checksums'],
+ ['wait', 'checkpoint-before-old-wal-removal'],
+ ['wakeup', 'checkpoint-before-old-wal-removal']
+);
+
+test_checksum_sequence('CHECKSUMS-ON-3', 'disabled', 'enable', 'off', @steps);
+
+## FIXME do similar sequences for the opposite direction (enabled -> disabled)
+
+$node->stop;
+done_testing();
--
2.54.0
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 07:43 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Tomas Vondra <[email protected]>
2 siblings, 1 reply; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-05-05 07:43 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi Hackers, Daniel,
Further testing this feature, I noticed that the cost_delay and cost_limit
arguments
to pg_enable_data_checksums() in practice have no effect.
It appears we have two independent issues in DataChecksumsWorkerMain():
(1) The worker writes the user-supplied values to VacuumCostDelay and
VacuumCostLimit (the GUC-bound globals in globals.c). However,
vacuum_delay_point() reads vacuum_cost_delay / vacuum_cost_limit
declared in vacuum.c. The two pairs are kept in sync only by
VacuumUpdateCosts(), which the worker never calls. Therefore, the napping
formula always sees the defaults (vacuum_cost_delay = 0) and never
sleeps.
(2) The worker also resets VacuumCostPageHit/Miss/Dirty to 0 at startup.
With all per-page weights at zero, VacuumCostBalance never reaches
vacuum_cost_limit, which would defeat the throttling on its own even
if (1) were fixed.
Repro:
Create a database and load data (say 3 GB)
SELECT pg_disable_data_checksums();
SELECT pg_enable_data_checksums(100, 1); -- 100 ms/page, balance limit 1
Without the fix, this completes in ~10 seconds and pg_stat_activity
never shows wait_event = VacuumDelay for the worker. With even moderate
parameters (e.g. (50, 200)) the worker is continuously in VacuumDelay
after the patch, and total runtime stretches as one would expect.
Also manually tested with cost_delay 0 and higher cost limits.
Attached a patch to fix this.
Thanks,
Satya
Attachments:
[application/octet-stream] 0001-Apply-data-checksum-worker-throttling-parameters.patch (2.8K, ../../CAHg+QDeevH6aTyWdXYBJW0wOmfoZy66gDi5TfinK_dXeCrHQLg@mail.gmail.com/3-0001-Apply-data-checksum-worker-throttling-parameters.patch)
download | inline diff:
From 967a25ec5ad9d515a432360dfb9c67d92d8ea491 Mon Sep 17 00:00:00 2001
From: Satya Narlapuram <[email protected]>
Date: Tue, 5 May 2026 07:12:10 +0000
Subject: [PATCH] Apply data-checksum worker throttling parameters
The DataChecksumsWorker accepts cost_delay and cost_limit parameters from
pg_enable_data_checksums() so users can throttle the rewrite I/O caused by
enabling checksums online, but the values were silently ignored:
* The worker assigned the user-supplied values to VacuumCostDelay and
VacuumCostLimit (the GUC-bound globals in globals.c), but
vacuum_delay_point() consults vacuum_cost_delay/vacuum_cost_limit
declared in vacuum.c. The two pairs are kept in sync by
VacuumUpdateCosts(), which the worker never called. As a result the
napping formula in vacuum_delay_point() always saw the boot defaults
(vacuum_cost_delay = 0), so no throttling occurred.
* The worker also reset VacuumCostPageHit/Miss/Dirty to 0 at startup.
With all per-page weights at zero, VacuumCostBalance never reaches
vacuum_cost_limit, which would defeat the throttling on its own even
if the first issue had not existed.
Fix both issues by calling VacuumUpdateCosts() after assigning the user
values (both at worker startup and on the runtime cost-update path), and
by leaving the page-cost weights at their GUC-controlled defaults.
---
src/backend/postmaster/datachecksum_state.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index d0d6acdd..c7c05933 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -1538,14 +1538,14 @@ DataChecksumsWorkerMain(Datum arg)
* provides rather than inventing something bespoke. This is an internal
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
+ *
+ * VacuumUpdateCosts() propagates the values to the variables actually
+ * read by vacuum_delay_point().
*/
VacuumCostDelay = DataChecksumState->cost_delay;
VacuumCostLimit = DataChecksumState->cost_limit;
- VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumUpdateCosts();
VacuumCostBalance = 0;
- VacuumCostPageHit = 0;
- VacuumCostPageMiss = 0;
- VacuumCostPageDirty = 0;
/*
* Create and set the vacuum strategy as our buffer strategy.
@@ -1602,7 +1602,7 @@ DataChecksumsWorkerMain(Datum arg)
costs_updated = true;
VacuumCostDelay = DataChecksumState->launch_cost_delay;
VacuumCostLimit = DataChecksumState->launch_cost_limit;
- VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumUpdateCosts();
DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
--
2.43.0
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 13:46 Daniel Gustafsson <[email protected]>
parent: SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-05-05 13:46 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> While further testing this feature, I realized that ProcessSingleRelationFork()
> unconditionally called log_newpage_buffer() for every page of every relation
> during pg_enable_data_checksums(). This included unlogged relations,
> which by definition never generate WAL for data changes and are reset to their
> init fork on any recovery.
I've tested your patch, and also expanded the test you wrote a little with a
persistence change.
> Further testing this feature, I noticed that the cost_delay and cost_limit arguments
> to pg_enable_data_checksums() in practice have no effect.
Ugh, the API for updating the costs changed between when this code was written
and tested, and when it was committed (and since I was the one committing the
new API I really should've caught that). Thanks for the report and fix!
While hacking on your patches I realized that the regexes for finding page
verification failures in the logs were anchoring at the wrong point, the
attached 0003 fixes that.
Attached are editorialized versions of the patches, as well as my testfix, that
I'm planning to go ahead with.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] 0003-Fix-regex-searching-for-page-verification-failures-i.patch (5.4K, ../../[email protected]/2-0003-Fix-regex-searching-for-page-verification-failures-i.patch)
download | inline diff:
From 271b7a7d59dba6e6230d03b528c1d3807df8cbb0 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 5 May 2026 15:09:41 +0200
Subject: [PATCH 3/3] Fix regex searching for page verification failures in
tests
The test for finding page verification failures in the logfiles
were missing the /m modifier to make sure it anchors to every
newline in the search space buffer, and not just the last one.
Spotted while adding a test for the recently reported issue
with excessive WAL for unlogged relations.
Author: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com
---
.../modules/test_checksums/t/006_pgbench_single.pl | 6 +++---
.../modules/test_checksums/t/007_pgbench_standby.pl | 12 ++++++------
src/test/modules/test_checksums/t/008_pitr.pl | 2 +-
src/test/modules/test_checksums/t/009_fpi.pl | 2 +-
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
index f5ccc1b5b08..b0ccf05fce9 100644
--- a/src/test/modules/test_checksums/t/006_pgbench_single.pl
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -211,7 +211,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
$node_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in primary log (during WAL recovery)"
);
$node_loglocation = -s $node->logfile;
@@ -249,7 +249,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
$node_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in primary log (outside WAL recovery)"
);
$node_loglocation = -s $node->logfile;
@@ -276,7 +276,7 @@ my $log =
PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in primary log");
$node_loglocation = -s $node->logfile;
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
index 0b3996f1d69..38e17ca38ee 100644
--- a/src/test/modules/test_checksums/t/007_pgbench_standby.pl
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -270,7 +270,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
$node_primary_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in primary log (during WAL recovery)"
);
$node_primary_loglocation = -s $node_primary->logfile;
@@ -299,7 +299,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
$node_standby_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in standby_1 log (during WAL recovery)"
);
$node_standby_loglocation = -s $node_standby->logfile;
@@ -341,7 +341,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
$node_primary_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in primary log (outside WAL recovery)"
);
$node_primary_loglocation = -s $node_primary->logfile;
@@ -363,7 +363,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
$node_standby_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in standby_1 log (outside WAL recovery)"
);
$node_standby_loglocation = -s $node_standby->logfile;
@@ -394,14 +394,14 @@ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
$node_primary_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in primary log");
$node_primary_loglocation = -s $node_primary->logfile;
$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile,
$node_standby_loglocation);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in standby_1 log");
$node_standby_loglocation = -s $node_standby->logfile;
diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl
index 1f8176686fd..913377c9d32 100644
--- a/src/test/modules/test_checksums/t/008_pitr.pl
+++ b/src/test/modules/test_checksums/t/008_pitr.pl
@@ -186,7 +186,7 @@ $node_pitr->stop;
my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in pitr log");
done_testing();
diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl
index a1cea91f787..1a7dcd0e15d 100644
--- a/src/test/modules/test_checksums/t/009_fpi.pl
+++ b/src/test/modules/test_checksums/t/009_fpi.pl
@@ -58,7 +58,7 @@ $node->stop;
my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0);
unlike(
$log,
- qr/page verification failed,.+\d$/,
+ qr/page verification failed,.+\d$/m,
"no checksum validation errors in server log");
done_testing();
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0002-Apply-data-checksum-worker-throttling-parameters.patch (2.5K, ../../[email protected]/3-0002-Apply-data-checksum-worker-throttling-parameters.patch)
download | inline diff:
From e62cc373ce6b7f93018b7dd541b89fae0dafb332 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 5 May 2026 15:00:24 +0200
Subject: [PATCH 2/3] Apply data-checksum worker throttling parameters
The DataChecksumsWorker accepts cost_delay and cost_limit parameters
from pg_enable_data_checksums() so users can throttle the I/O caused
by enabling checksums. Due to the API for setting the cost parameters
changing between when the code was written, and when it was committed
the new cost update function call was omitted and thus the parameters
were silently ignored.
Fix by calling VacuumUpdateCosts() after assigning the parameters
(both during worker startup and on the runtime cost-update path), and
by leaving the page-cost weights at their GUC-controlled defaults.
Author: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDeevH6aTyWdXYBJW0wOmfoZy66gDi5TfinK_dXeCrHQLg@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 535da8b0c4a..7cdc662c7a6 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -1543,14 +1543,14 @@ DataChecksumsWorkerMain(Datum arg)
* provides rather than inventing something bespoke. This is an internal
* implementation detail and care should be taken to avoid it bleeding
* through to the user to avoid confusion.
+ *
+ * VacuumUpdateCosts() propagates the values to the variables actually
+ * read by vacuum_delay_point().
*/
VacuumCostDelay = DataChecksumState->cost_delay;
VacuumCostLimit = DataChecksumState->cost_limit;
- VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumUpdateCosts();
VacuumCostBalance = 0;
- VacuumCostPageHit = 0;
- VacuumCostPageMiss = 0;
- VacuumCostPageDirty = 0;
/*
* Create and set the vacuum strategy as our buffer strategy.
@@ -1607,7 +1607,7 @@ DataChecksumsWorkerMain(Datum arg)
costs_updated = true;
VacuumCostDelay = DataChecksumState->launch_cost_delay;
VacuumCostLimit = DataChecksumState->launch_cost_limit;
- VacuumCostActive = (VacuumCostDelay > 0);
+ VacuumUpdateCosts();
DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (5.8K, ../../[email protected]/4-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch)
download | inline diff:
From 7f5addd7b93132dbb4d8a9a0e45a961df4470805 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 5 May 2026 14:54:23 +0200
Subject: [PATCH 1/3] Skip WAL for unlogged relations during online checksum
enable
ProcessSingleRelationFork() unconditionally generated an FPI WAL
record for every page of every relation when enabling checksums.
Unlogged relations, which by definition never generate WAL for
data changes, were not exempt which generated excessive WAL to
be emitted.
Fix by guarding the FPI WAL record call with RelationNeedsWAL()
to avoid emitting WAL for unlogged relations. Unlogged pages
are still dirtied to ensure the checksum is written to disk at
the next checkpoint.
A test which creates an unlogged table, enables checksums, and
verifies via pg_relation_size() on the standby has been added.
Author: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 9 ++-
.../test_checksums/t/003_standby_restarts.pl | 72 +++++++++++++++++++
2 files changed, 79 insertions(+), 2 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index d0d6acdd6a2..535da8b0c4a 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -690,11 +690,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
* at one point in the past, so only when checksums are first on, then
* off, and then turned on again. TODO: investigate if this could be
* avoided if the checksum is calculated to be correct and wal_level
- * is set to "minimal",
+ * is set to "minimal".
+ *
+ * Unlogged relations don't need WAL since they are reset to their
+ * init fork on recovery. We still dirty the buffer so that the
+ * checksum is written to disk at the next checkpoint.
*/
START_CRIT_SECTION();
MarkBufferDirty(buf);
- log_newpage_buffer(buf, false);
+ if (RelationNeedsWAL(reln))
+ log_newpage_buffer(buf, false);
END_CRIT_SECTION();
UnlockReleaseBuffer(buf);
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index 11e15c9d734..b79a06b9b32 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -115,6 +115,78 @@ $result =
$node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
is($result, "19998", 'ensure we can safely read all data without checksums');
+# ---------------------------------------------------------------------------
+# Test that enabling checksums does not emit WAL for unlogged relations.
+# Unlogged relations are wiped on recovery, so FPIs for them would be
+# pointless and waste WAL traffic / standby I/O.
+#
+
+$node_primary->safe_psql('postgres',
+ 'CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,1000) AS a;'
+);
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Get the relfilenode and database OID so we can inspect the filesystem
+my $unlogged_rfn = $node_primary->safe_psql('postgres',
+ "SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';");
+my $db_oid = $node_primary->safe_psql('postgres',
+ "SELECT oid FROM pg_database WHERE datname = 'postgres';");
+
+# Verify the standby only has the init fork (no main fork)
+my $standby_datadir = $node_standby->data_dir;
+ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+ 'standby has no main fork for unlogged table before enable');
+
+# Re-enable data checksums
+enable_data_checksums($node_primary, wait => 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+# After standby replays, the unlogged main file must still not exist.
+# If the bug were present, FPI replay would materialize the full table.
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+ 'standby has no main fork for unlogged table after enable');
+
+# Verify unlogged relation size is 0 on the standby (main fork missing)
+my $standby_size = $node_standby->safe_psql('postgres',
+ "SELECT pg_relation_size('unlogged_tbl', 'main');");
+is($standby_size, '0',
+ 'unlogged table has zero size on standby after checksum enable');
+
+# Unlogged table should still be readable on primary
+$result =
+ $node_primary->safe_psql('postgres', 'SELECT count(*) FROM unlogged_tbl;');
+is($result, '1000',
+ 'unlogged table readable on primary after checksum enable');
+
+# Alter persistence to logged, and make sure we can read it on both the primary
+# and standby without any page verification errors in the logfiles.
+$node_primary->safe_psql('postgres', 'ALTER TABLE unlogged_tbl SET logged;');
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+$result =
+ $node_primary->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;');
+is($result, '500500', 'previously unlogged table can be read on primary');
+$result =
+ $node_standby->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;');
+is($result, '500500', 'previously unlogged table can be read on standby');
+
$node_standby->stop;
$node_primary->stop;
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/m,
+ "no checksum validation errors in primary log");
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/m,
+ "no checksum validation errors in standby log");
+
done_testing();
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 14:24 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-05-05 14:24 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Tue, May 5, 2026 at 6:46 AM Daniel Gustafsson <[email protected]> wrote:
> > While further testing this feature, I realized that
> ProcessSingleRelationFork()
> > unconditionally called log_newpage_buffer() for every page of every
> relation
> > during pg_enable_data_checksums(). This included unlogged relations,
> > which by definition never generate WAL for data changes and are reset to
> their
> > init fork on any recovery.
>
>
> I've tested your patch, and also expanded the test you wrote a little with
> a
> persistence change.
>
> > Further testing this feature, I noticed that the cost_delay and
> cost_limit arguments
> > to pg_enable_data_checksums() in practice have no effect.
>
> Ugh, the API for updating the costs changed between when this code was
> written
> and tested, and when it was committed (and since I was the one committing
> the
> new API I really should've caught that). Thanks for the report and fix!
>
Any thoughts on adding an injection point test to verify the values are
configured correctly?
This can be a different patch, perhaps included in Tomas' tests?
>
> While hacking on your patches I realized that the regexes for finding page
> verification failures in the logs were anchoring at the wrong point, the
> attached 0003 fixes that.
>
> Attached are editorialized versions of the patches, as well as my testfix,
> that
> I'm planning to go ahead with.
>
LGTM. Thanks!
Thanks,
Satya
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 15:21 Ayush Tiwari <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Ayush Tiwari @ 2026-05-05 15:21 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Tue, 5 May 2026 at 19:16, Daniel Gustafsson <[email protected]> wrote:
> > While further testing this feature, I realized that
> ProcessSingleRelationFork()
> > unconditionally called log_newpage_buffer() for every page of every
> relation
> > during pg_enable_data_checksums(). This included unlogged relations,
> > which by definition never generate WAL for data changes and are reset to
> their
> > init fork on any recovery.
>
>
> I've tested your patch, and also expanded the test you wrote a little with
> a
> persistence change.
>
>
I tested the patches, it mostly looks good.
I've a small concern in 0001. The new guard uses only
RelationNeedsWAL(reln),
but ProcessSingleRelationByOid() iterates all forks. For unlogged
relations,
the init fork is special, there are several existing call sites that
preserve
WAL for INIT_FORKNUM, for example using
RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
and catalog/storage.c notes that unlogged init forks need WAL and sync.
So I think the condition in ProcessSingleRelationFork() should preserve the
init-fork case, e.g.
if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
log_newpage_buffer(buf, false);
It would also be good to extend the new test so it exercises a non-empty
init
fork, perhaps with an unlogged table that has an index, and then verifies
the
standby/recovery behavior.
0002 and 0003 look good to me.
Regards,
Ayush
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 19:08 Daniel Gustafsson <[email protected]>
parent: Ayush Tiwari <[email protected]>
0 siblings, 2 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-05-05 19:08 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> wrote:
> I've a small concern in 0001. The new guard uses only RelationNeedsWAL(reln),
> but ProcessSingleRelationByOid() iterates all forks. For unlogged relations,
> the init fork is special, there are several existing call sites that preserve
> WAL for INIT_FORKNUM, for example using
>
> RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
>
> and catalog/storage.c notes that unlogged init forks need WAL and sync.
>
> So I think the condition in ProcessSingleRelationFork() should preserve the
> init-fork case, e.g.
>
> if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
> log_newpage_buffer(buf, false);
Which failure scenario are you thinking about here? When dealing with the
catalog relation I can see the need but here we are reading, and writing, data
pages. In which case would we need to issue an FPI for an unlogged relation
init fork? I might be missing something obvious here.
> 0002 and 0003 look good to me.
Thanks for looking!
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 19:18 Ayush Tiwari <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 1 reply; 60+ messages in thread
From: Ayush Tiwari @ 2026-05-05 19:18 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote:
> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]>
> wrote:
>
> > I've a small concern in 0001. The new guard uses only
> RelationNeedsWAL(reln),
> > but ProcessSingleRelationByOid() iterates all forks. For unlogged
> relations,
> > the init fork is special, there are several existing call sites that
> preserve
> > WAL for INIT_FORKNUM, for example using
> >
> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
> >
> > and catalog/storage.c notes that unlogged init forks need WAL and sync.
> >
> > So I think the condition in ProcessSingleRelationFork() should preserve
> the
> > init-fork case, e.g.
> >
> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
> > log_newpage_buffer(buf, false);
>
> Which failure scenario are you thinking about here? When dealing with the
> catalog relation I can see the need but here we are reading, and writing,
> data
> pages. In which case would we need to issue an FPI for an unlogged
> relation
> init fork? I might be missing something obvious here.
>
The case I was thinking about is not the unlogged relation contents
themselves, but the init fork used as the reset template. Some unlogged
indexes can have initialized pages in the init fork, and recovery later
copies
that fork to the main fork when resetting unlogged relations.
So my concern was that, during online checksum enable, we might update the
checksum state of an init-fork page on the primary but not WAL-log an FPI
for
it because RelationNeedsWAL(reln) is false. Then a standby, or recovery
after
a crash, could still have the old version of that init fork. If that fork
is
later copied to the main fork after checksums are enabled, it might lead to
checksum verification failures?
Maybe there is another guarantee that makes this impossible, but I did not
see
it from the patch/test. That is why I wondered whether the condition should
preserve the existing special treatment for INIT_FORKNUM.
Regards,
Ayush
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 19:19 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Daniel Gustafsson <[email protected]>
1 sibling, 0 replies; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-05-05 19:19 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi
On Tue, May 5, 2026 at 12:08 PM Daniel Gustafsson <[email protected]> wrote:
> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]>
> wrote:
>
> > I've a small concern in 0001. The new guard uses only
> RelationNeedsWAL(reln),
> > but ProcessSingleRelationByOid() iterates all forks. For unlogged
> relations,
> > the init fork is special, there are several existing call sites that
> preserve
> > WAL for INIT_FORKNUM, for example using
> >
> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
> >
> > and catalog/storage.c notes that unlogged init forks need WAL and sync.
> >
> > So I think the condition in ProcessSingleRelationFork() should preserve
> the
> > init-fork case, e.g.
> >
> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
> > log_newpage_buffer(buf, false);
>
> Which failure scenario are you thinking about here? When dealing with the
> catalog relation I can see the need but here we are reading, and writing,
> data
> pages. In which case would we need to issue an FPI for an unlogged
> relation
> init fork? I might be missing something obvious here.
Good catch,IIUC init page has valid checksum and when we set checksum on
primary I think we need to pass it to standby. Otherwise, after failover we
may see invalid checksum for unlogged relations. I haven’t validated it,
will test it and update the patch.
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 19:45 SATYANARAYANA NARLAPURAM <[email protected]>
parent: Ayush Tiwari <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-05-05 19:45 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Tue, May 5, 2026 at 12:19 PM Ayush Tiwari <[email protected]>
wrote:
> Hi,
>
> On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote:
>
>> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]>
>> wrote:
>>
>> > I've a small concern in 0001. The new guard uses only
>> RelationNeedsWAL(reln),
>> > but ProcessSingleRelationByOid() iterates all forks. For unlogged
>> relations,
>> > the init fork is special, there are several existing call sites that
>> preserve
>> > WAL for INIT_FORKNUM, for example using
>> >
>> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
>> >
>> > and catalog/storage.c notes that unlogged init forks need WAL and sync.
>> >
>> > So I think the condition in ProcessSingleRelationFork() should preserve
>> the
>> > init-fork case, e.g.
>> >
>> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
>> > log_newpage_buffer(buf, false);
>>
>> Which failure scenario are you thinking about here? When dealing with the
>> catalog relation I can see the need but here we are reading, and writing,
>> data
>> pages. In which case would we need to issue an FPI for an unlogged
>> relation
>> init fork? I might be missing something obvious here.
>>
>
> The case I was thinking about is not the unlogged relation contents
> themselves, but the init fork used as the reset template. Some unlogged
> indexes can have initialized pages in the init fork, and recovery later
> copies
> that fork to the main fork when resetting unlogged relations.
>
> So my concern was that, during online checksum enable, we might update the
> checksum state of an init-fork page on the primary but not WAL-log an FPI
> for
> it because RelationNeedsWAL(reln) is false. Then a standby, or recovery
> after
> a crash, could still have the old version of that init fork. If that fork
> is
> later copied to the main fork after checksums are enabled, it might lead to
> checksum verification failures?
>
> Maybe there is another guarantee that makes this impossible, but I did not
> see
> it from the patch/test. That is why I wondered whether the condition
> should
> preserve the existing special treatment for INIT_FORKNUM.
>
It is a bug in the code, I added a test in the v2 patch to test this
scenario and the test
failed earlier.
Thanks,
Satya
>
Attachments:
[application/octet-stream] v2-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (6.3K, ../../CAHg+QDfR0NHqWmE50R42Wi=Bz7Gw9Q-p84TA=vragN_nfjmnew@mail.gmail.com/3-v2-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch)
download | inline diff:
From 7f5addd7b93132dbb4d8a9a0e45a961df4470806 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 5 May 2026 14:54:23 +0200
Subject: [PATCH v3 1/3] Skip WAL for unlogged relations during online checksum
enable
ProcessSingleRelationFork() unconditionally generated an FPI WAL
record for every page of every relation when enabling checksums.
Unlogged relations, which by definition never generate WAL for
data changes, were not exempt which generated excessive WAL to
be emitted.
Fix by guarding the FPI WAL record call with RelationNeedsWAL()
to avoid emitting WAL for unlogged main forks. Unlogged pages
are still dirtied to ensure the checksum is written to disk at
the next checkpoint.
The init fork remains WAL-logged even for unlogged relations,
because that is what the standby uses to materialize the relation
after promotion (see ResetUnloggedRelations()). Skipping init-fork
WAL would leave the standby with a stale init fork that, once
copied to the main fork on promotion, would fail checksum
verification on every read of the unlogged relation.
A test which creates an unlogged table with an index, enables
checksums, promotes the standby, and verifies that the unlogged
relation and its indexes are still readable post-promotion has
been added.
Author: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 16 +++-
.../test_checksums/t/003_standby_restarts.pl | 65 ++++++++++++++++++-
2 files changed, 78 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index c7c0593345..fb29662423 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -690,11 +690,23 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
* at one point in the past, so only when checksums are first on, then
* off, and then turned on again. TODO: investigate if this could be
* avoided if the checksum is calculated to be correct and wal_level
- * is set to "minimal",
+ * is set to "minimal".
+ *
+ * Unlogged relations don't need WAL since they are reset to their
+ * init fork on recovery. We still dirty the buffer so that the
+ * checksum is written to disk at the next checkpoint.
+ *
+ * The init fork is an exception: it is WAL-logged so the standby
+ * can materialize the relation after promotion (see
+ * ResetUnloggedRelations()). Skipping it here would leave the
+ * standby with a stale init fork that, once copied to the main
+ * fork on promotion, would fail checksum verification on every
+ * read.
*/
START_CRIT_SECTION();
MarkBufferDirty(buf);
- log_newpage_buffer(buf, false);
+ if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
+ log_newpage_buffer(buf, false);
END_CRIT_SECTION();
UnlockReleaseBuffer(buf);
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index 11e15c9d73..26b68b98e6 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -115,6 +115,69 @@ $result =
$node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
is($result, "19998", 'ensure we can safely read all data without checksums');
-$node_standby->stop;
+# ---------------------------------------------------------------------------
+# Test that enabling checksums on a cluster with unlogged relations does not
+# break those relations after a standby promotion. Unlogged relations only
+# WAL-log their init fork; that init fork is what ResetUnloggedRelations()
+# copies to the main fork on promotion. If checksum-enable on the primary
+# does not WAL-log init-fork rewrites, the standby keeps a stale init fork
+# and the post-promotion main fork fails verification on every read.
+#
+
+# Use a btree index so the init fork is non-trivial (one metapage)
+$node_primary->safe_psql(
+ 'postgres', q[
+ CREATE UNLOGGED TABLE unlogged_tbl (id int PRIMARY KEY,
+ payload text);
+ INSERT INTO unlogged_tbl
+ SELECT g, repeat('x', 100) FROM generate_series(1, 1000) g;
+ CREATE INDEX unlogged_tbl_payload_idx ON unlogged_tbl (payload);
+]);
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Re-enable data checksums and wait for both nodes to converge to "on"
+enable_data_checksums($node_primary, wait => 'on');
+wait_for_checksum_state($node_standby, 'on');
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Promote the standby and verify the unlogged relation is still usable.
+# Without the init-fork WAL fix, every read of the index would fail with
+# "page verification failed, calculated checksum X but expected 0".
$node_primary->stop;
+$node_standby->promote;
+
+$result =
+ $node_standby->safe_psql('postgres', 'SELECT count(*) FROM unlogged_tbl;');
+is($result, '0',
+ 'unlogged table readable on promoted standby (truncated as expected)');
+
+$node_standby->safe_psql('postgres',
+ "INSERT INTO unlogged_tbl SELECT g, repeat('y',100) FROM generate_series(1,100) g;"
+);
+$result = $node_standby->safe_psql('postgres',
+ 'SET enable_seqscan = off; SELECT id FROM unlogged_tbl WHERE id = 50;');
+is($result, '50', 'indexed lookup on promoted standby returns expected row');
+
+$node_standby->safe_psql('postgres', 'CREATE EXTENSION IF NOT EXISTS amcheck;');
+$node_standby->safe_psql('postgres',
+ "SELECT bt_index_check('unlogged_tbl_pkey'::regclass);");
+$node_standby->safe_psql('postgres',
+ "SELECT bt_index_check('unlogged_tbl_payload_idx'::regclass);");
+
+$node_standby->stop;
+
+# Final sanity sweep of the logs for any checksum failures
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/m,
+ "no checksum validation errors in primary log");
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/m,
+ "no checksum validation errors in standby log");
+
done_testing();
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 20:05 SATYANARAYANA NARLAPURAM <[email protected]>
parent: SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-05-05 20:05 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Tue, May 5, 2026 at 12:45 PM SATYANARAYANA NARLAPURAM <
[email protected]> wrote:
> Hi,
>
> On Tue, May 5, 2026 at 12:19 PM Ayush Tiwari <[email protected]>
> wrote:
>
>> Hi,
>>
>> On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote:
>>
>>> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]>
>>> wrote:
>>>
>>> > I've a small concern in 0001. The new guard uses only
>>> RelationNeedsWAL(reln),
>>> > but ProcessSingleRelationByOid() iterates all forks. For unlogged
>>> relations,
>>> > the init fork is special, there are several existing call sites that
>>> preserve
>>> > WAL for INIT_FORKNUM, for example using
>>> >
>>> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
>>> >
>>> > and catalog/storage.c notes that unlogged init forks need WAL and sync.
>>> >
>>> > So I think the condition in ProcessSingleRelationFork() should
>>> preserve the
>>> > init-fork case, e.g.
>>> >
>>> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
>>> > log_newpage_buffer(buf, false);
>>>
>>> Which failure scenario are you thinking about here? When dealing with
>>> the
>>> catalog relation I can see the need but here we are reading, and
>>> writing, data
>>> pages. In which case would we need to issue an FPI for an unlogged
>>> relation
>>> init fork? I might be missing something obvious here.
>>>
>>
>> The case I was thinking about is not the unlogged relation contents
>> themselves, but the init fork used as the reset template. Some unlogged
>> indexes can have initialized pages in the init fork, and recovery later
>> copies
>> that fork to the main fork when resetting unlogged relations.
>>
>> So my concern was that, during online checksum enable, we might update the
>> checksum state of an init-fork page on the primary but not WAL-log an FPI
>> for
>> it because RelationNeedsWAL(reln) is false. Then a standby, or recovery
>> after
>> a crash, could still have the old version of that init fork. If that
>> fork is
>> later copied to the main fork after checksums are enabled, it might lead
>> to
>> checksum verification failures?
>>
>> Maybe there is another guarantee that makes this impossible, but I did
>> not see
>> it from the patch/test. That is why I wondered whether the condition
>> should
>> preserve the existing special treatment for INIT_FORKNUM.
>>
>
> It is a bug in the code, I added a test in the v2 patch to test this
> scenario and the test
> failed earlier.
>
Please find the latest v3. Earlier I took dependency on amcheck and removed
it in v3
and instead added queries that forces necessary checks.
Thanks,
Satya
Attachments:
[application/octet-stream] v3-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (8.3K, ../../CAHg+QDfEpF0S7S6rzgjKrvq6HrE_CdKg2KM9mS7dKZQhqKuVtA@mail.gmail.com/3-v3-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch)
download | inline diff:
From 7f5addd7b93132dbb4d8a9a0e45a961df4470806 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Tue, 5 May 2026 14:54:23 +0200
Subject: [PATCH v3 1/3] Skip WAL for unlogged relations during online checksum
enable
ProcessSingleRelationFork() unconditionally generated an FPI WAL
record for every page of every relation when enabling checksums.
Unlogged relations, which by definition never generate WAL for
data changes, were not exempt which generated excessive WAL to
be emitted.
Fix by guarding the FPI WAL record call with RelationNeedsWAL()
to avoid emitting WAL for unlogged main forks. Unlogged pages
are still dirtied to ensure the checksum is written to disk at
the next checkpoint.
The init fork remains WAL-logged even for unlogged relations,
because that is what the standby uses to materialize the relation
after promotion (see ResetUnloggedRelations()). Skipping init-fork
WAL would leave the standby with a stale init fork that, once
copied to the main fork on promotion, would fail checksum
verification on every read of the unlogged relation.
A test which creates an unlogged table with an index, enables
checksums, promotes the standby, and verifies that the unlogged
relation and its indexes are still readable post-promotion has
been added.
Author: SATYANARAYANA NARLAPURAM <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com
---
src/backend/postmaster/datachecksum_state.c | 16 ++-
.../test_checksums/t/003_standby_restarts.pl | 111 ++++++++++++++++++++-
2 files changed, 124 insertions(+), 3 deletions(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index c7c0593345..fb29662423 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -690,11 +690,23 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
* at one point in the past, so only when checksums are first on, then
* off, and then turned on again. TODO: investigate if this could be
* avoided if the checksum is calculated to be correct and wal_level
- * is set to "minimal",
+ * is set to "minimal".
+ *
+ * Unlogged relations don't need WAL since they are reset to their
+ * init fork on recovery. We still dirty the buffer so that the
+ * checksum is written to disk at the next checkpoint.
+ *
+ * The init fork is an exception: it is WAL-logged so the standby
+ * can materialize the relation after promotion (see
+ * ResetUnloggedRelations()). Skipping it here would leave the
+ * standby with a stale init fork that, once copied to the main
+ * fork on promotion, would fail checksum verification on every
+ * read.
*/
START_CRIT_SECTION();
MarkBufferDirty(buf);
- log_newpage_buffer(buf, false);
+ if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
+ log_newpage_buffer(buf, false);
END_CRIT_SECTION();
UnlockReleaseBuffer(buf);
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index 11e15c9d73..5bbf38ed21 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -115,6 +115,115 @@ $result =
$node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
is($result, "19998", 'ensure we can safely read all data without checksums');
-$node_standby->stop;
+# ---------------------------------------------------------------------------
+# Test that enabling checksums does not emit WAL for unlogged relations.
+# Unlogged relations are wiped on recovery, so FPIs for them would be
+# pointless and waste WAL traffic / standby I/O.
+#
+# Additionally, exercise standby promotion to ensure the init fork of an
+# unlogged relation is still WAL-logged during checksum enable -- otherwise
+# the standby keeps a stale init fork and the post-promotion main fork
+# fails verification on every read (see ResetUnloggedRelations()). Both
+# tables must exist BEFORE enable_data_checksums() so that their init
+# forks get re-checksummed during the enable sweep.
+#
+
+$node_primary->safe_psql('postgres',
+ 'CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,1000) AS a;'
+);
+# Use a btree index so the init fork is non-trivial (one metapage).
+$node_primary->safe_psql(
+ 'postgres', q[
+ CREATE UNLOGGED TABLE unlogged_promo (id int PRIMARY KEY,
+ payload text);
+ INSERT INTO unlogged_promo
+ SELECT g, repeat('x', 100) FROM generate_series(1, 1000) g;
+ CREATE INDEX unlogged_promo_payload_idx ON unlogged_promo (payload);
+]);
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+# Get the relfilenode and database OID so we can inspect the filesystem
+my $unlogged_rfn = $node_primary->safe_psql('postgres',
+ "SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';");
+my $db_oid = $node_primary->safe_psql('postgres',
+ "SELECT oid FROM pg_database WHERE datname = 'postgres';");
+
+# Verify the standby only has the init fork (no main fork)
+my $standby_datadir = $node_standby->data_dir;
+ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+ 'standby has no main fork for unlogged table before enable');
+
+# Re-enable data checksums
+enable_data_checksums($node_primary, wait => 'on');
+wait_for_checksum_state($node_standby, 'on');
+
+# After standby replays, the unlogged main file must still not exist.
+# If the bug were present, FPI replay would materialize the full table.
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn",
+ 'standby has no main fork for unlogged table after enable');
+
+# Verify unlogged relation size is 0 on the standby (main fork missing)
+my $standby_size = $node_standby->safe_psql('postgres',
+ "SELECT pg_relation_size('unlogged_tbl', 'main');");
+is($standby_size, '0',
+ 'unlogged table has zero size on standby after checksum enable');
+
+# Unlogged table should still be readable on primary
+$result =
+ $node_primary->safe_psql('postgres', 'SELECT count(*) FROM unlogged_tbl;');
+is($result, '1000',
+ 'unlogged table readable on primary after checksum enable');
+
+# Alter persistence to logged, and make sure we can read it on both the primary
+# and standby without any page verification errors in the logfiles.
+$node_primary->safe_psql('postgres', 'ALTER TABLE unlogged_tbl SET logged;');
+$node_primary->wait_for_catchup($node_standby, 'replay',
+ $node_primary->lsn('insert'));
+
+$result =
+ $node_primary->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;');
+is($result, '500500', 'previously unlogged table can be read on primary');
+$result =
+ $node_standby->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;');
+is($result, '500500', 'previously unlogged table can be read on standby');
+
+# ---------------------------------------------------------------------------
+# Promote the standby and verify the unlogged_promo relation (created above
+# before the enable sweep) is still usable. Without the init-fork WAL fix,
+# every read of the index would fail with
+# "page verification failed, calculated checksum X but expected 0".
+#
$node_primary->stop;
+$node_standby->promote;
+
+$result =
+ $node_standby->safe_psql('postgres',
+ 'SELECT count(*) FROM unlogged_promo;');
+is($result, '0',
+ 'unlogged table readable on promoted standby (truncated as expected)');
+
+$node_standby->safe_psql('postgres',
+ "INSERT INTO unlogged_promo SELECT g, repeat('y',100) FROM generate_series(1,100) g;"
+);
+$result = $node_standby->safe_psql('postgres',
+ 'SET enable_seqscan = off; SELECT id FROM unlogged_promo WHERE id = 50;');
+is($result, '50', 'indexed lookup on promoted standby returns expected row');
+
+$node_standby->stop;
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/m,
+ "no checksum validation errors in primary log");
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, 0);
+unlike(
+ $log,
+ qr/page verification failed,.+\d$/m,
+ "no checksum validation errors in standby log");
+
done_testing();
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-05 21:01 Ayush Tiwari <[email protected]>
parent: SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Ayush Tiwari @ 2026-05-05 21:01 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On Wed, 6 May 2026 at 01:36, SATYANARAYANA NARLAPURAM <
[email protected]> wrote:
> Hi,
>
> On Tue, May 5, 2026 at 12:45 PM SATYANARAYANA NARLAPURAM <
> [email protected]> wrote:
>
>> Hi,
>>
>> On Tue, May 5, 2026 at 12:19 PM Ayush Tiwari <[email protected]>
>> wrote:
>>
>>> Hi,
>>>
>>> On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote:
>>>
>>>> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]>
>>>> wrote:
>>>>
>>>> > I've a small concern in 0001. The new guard uses only
>>>> RelationNeedsWAL(reln),
>>>> > but ProcessSingleRelationByOid() iterates all forks. For unlogged
>>>> relations,
>>>> > the init fork is special, there are several existing call sites that
>>>> preserve
>>>> > WAL for INIT_FORKNUM, for example using
>>>> >
>>>> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM
>>>> >
>>>> > and catalog/storage.c notes that unlogged init forks need WAL and
>>>> sync.
>>>> >
>>>> > So I think the condition in ProcessSingleRelationFork() should
>>>> preserve the
>>>> > init-fork case, e.g.
>>>> >
>>>> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
>>>> > log_newpage_buffer(buf, false);
>>>>
>>>> Which failure scenario are you thinking about here? When dealing with
>>>> the
>>>> catalog relation I can see the need but here we are reading, and
>>>> writing, data
>>>> pages. In which case would we need to issue an FPI for an unlogged
>>>> relation
>>>> init fork? I might be missing something obvious here.
>>>>
>>>
>>> The case I was thinking about is not the unlogged relation contents
>>> themselves, but the init fork used as the reset template. Some unlogged
>>> indexes can have initialized pages in the init fork, and recovery later
>>> copies
>>> that fork to the main fork when resetting unlogged relations.
>>>
>>> So my concern was that, during online checksum enable, we might update
>>> the
>>> checksum state of an init-fork page on the primary but not WAL-log an
>>> FPI for
>>> it because RelationNeedsWAL(reln) is false. Then a standby, or recovery
>>> after
>>> a crash, could still have the old version of that init fork. If that
>>> fork is
>>> later copied to the main fork after checksums are enabled, it might lead
>>> to
>>> checksum verification failures?
>>>
>>> Maybe there is another guarantee that makes this impossible, but I did
>>> not see
>>> it from the patch/test. That is why I wondered whether the condition
>>> should
>>> preserve the existing special treatment for INIT_FORKNUM.
>>>
>>
>> It is a bug in the code, I added a test in the v2 patch to test this
>> scenario and the test
>> failed earlier.
>>
>
> Please find the latest v3. Earlier I took dependency on amcheck and
> removed it in v3
> and instead added queries that forces necessary checks.
>
Thanks, the v3 patch addresses my concern.
I also checked the new test by
temporarily removing the INIT_FORKNUM exception, and it failed on the
promoted
standby while reading the unlogged relation, so the test does cover the init
fork failure scenario.
The code change looks right to me now.
Regards,
Ayush
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-06 15:22 Daniel Gustafsson <[email protected]>
parent: Ayush Tiwari <[email protected]>
0 siblings, 0 replies; 60+ messages in thread
From: Daniel Gustafsson @ 2026-05-06 15:22 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 5 May 2026, at 23:01, Ayush Tiwari <[email protected]> wrote:
> Thanks, the v3 patch addresses my concern.
Thanks both of you for the report, patch and testing. Now I see what you meant
yesterday and I agree with the conclusion. I've pushed 0001-0003 today with minor
tweaks.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-26 18:12 Tomas Vondra <[email protected]>
parent: Tomas Vondra <[email protected]>
2 siblings, 1 reply; 60+ messages in thread
From: Tomas Vondra @ 2026-05-26 18:12 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
Hi,
On 5/4/26 15:16, Tomas Vondra wrote:
> Hi,
>
> ... snip ...
>
> Let me briefly explain what the various TAP tests aim to do. From the
> very beginning, my main concern regarding this patch was race conditions
> when updating the shared state about effective data_checksum_version.
> Because the state is effectively split into about three or four places:
>
> * LocalDataChecksumVersion (local cache)
> * XLogCtl->data_checksum_version (XLogCtl->info_lck)
> * ControlFile->data_checksum_version (ControlFileLock)
> * state in control file on disk
>
> These pieces are protected by different locks, the protocol for updating
> and/or reading the various flags is not trivial (and some of the fixed
> issues were due to ControlFile->data_checksum_version being updated from
> a place that shouldn't have).
>
I still have a funny feeling about the "global checksum version" stored
in multiple places with separate locks, and the protocol when updating
them (e.g. which process does that, when, etc.). I haven't found any
fundamental correctness issue, though.
I kept looking for issues, and I realized StartupXLOG may not do the
right thing on the standby after promotion.
if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
{
XLogChecksums(PG_DATA_CHECKSUM_OFF);
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
ereport(...);
}
Consider this sequence of steps:
1) primary/standby cluster has checksums OFF
2) primary starts enabling checksums
3) primary moves to inprogress-on
4) standby receives that and moves to inprogress-on too
5) primary crashes / shuts down / ...
6) standby gets promoted, and does the StartupXLOG thing
7) standby moves from inprogress-on back to off
However, as there's no EmitProcSignalBarrier(), existing backends on the
hot standby won't be notified about the "off" change. And so there will
be a somewhat inconsistent checksum state, with new backends knowing
it's "off", and old backends thinking it's still inprogress-on.
It's not difficult to reproduce this. I guess the existing TAP tests may
not catch this because they always open a new connection when checking
the state, and so see the new (and correct) version.
I believe a similar issue happens for the inprogress-off case a couple
lines later, but I haven't tried reproducing that one.
Ultimately, neither of these cases should cause checksum failures,
because the "correct" new state is "off", and the old backends are not
verifying checksums either.
I suppose this means we should not be updating the checksum state
without emitting the barrier? I think all other places do that.
>
> ... snip ...
>
> The TAP 012 tests checksum change with a concurrent checkpoint, followed
> by a crash, and tests the final state. It pauses the change at an
> injection point, does a checkpoint, proceeds to the next injection
> point, crashes and does recovery. The expectation is that the final
> state "flips" at some injection point, once it gets further enough, and
> stays there. But what actually happens is this:
>
> a) test_checksum_transition(
> 'disabled', 'enable', undef,
> 'datachecksums-enable-inprogress-checksums-end',
> 'datachecksums-enable-checksums-start',
> 'off');
>
> b) test_checksum_transition(
> 'disabled', 'enable', undef,
> 'datachecksums-enable-checksums-start',
> 'datachecksums-enable-checksums-after-xlog',
> 'on');
>
> c) test_checksum_transition(
> 'disabled', 'enable', 'datachecksums-enable-checksums-start',
> 'datachecksums-enable-checksums-after-xlogs',
> 'datachecksums-enable-checksums-after-xlogctl',
> 'off');
>
> This says that if the checkpoint happens after
> 'datachecksums-enable-inprogress-checksums-end' or after
> 'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e.
> enabling checksums fails).
>
> But if the checkpoint happens after
> 'datachecksums-enable-checksums-start', we end up with "on" (after
> recovery).
>
> This is a bit surprising, because that injection point is before
> 'datachecksums-enable-checksums-after-xlog'. So the enabling process
> gets further and further, but the final state flips off -> on -> off,
> contradicting the expectation that it changes once.
>
> I haven't quite wrapped my head around it yet, but my understanding is
> this is due to a race condition between the checksum launcher (writing
> XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer
> (reading the shmem state and generating REDO).
>
> The launcher does this sequence of steps:
>
> 1) write XLOG2_CHECKSUMS with new state
> 2) update XLogCtl->data_checksum_version
> 3) update ControlFile->data_checksum_version
> 4) UpdateControlFile()
> 5) emits barrier
>
> while the checkpointer (CreateCheckPoint) does this:
>
> A) read XLogCtl->data_checksum_version (while holding insert locks)
> B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
> C) UpdateControlFile()
>
> The outcome depends on how exactly these two sequences interleave. For
> example, this can happen:
>
> 1) write XLOG2_CHECKSUMS with new state
> A) read XLogCtl->data_checksum_version (while holding insert locks)
> B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
> C) UpdateControlFile()
> 2) update XLogCtl->data_checksum_version
> 3) update ControlFile->data_checksum_version
> 4) UpdateControlFile()
> 5) emits barrier
>
> Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and
> so redo won't see it), but the checkpoint will still get the old
> checksum state from XLogCtl. And so the outcome is "off", per case (c).
>
> But it can also happen what case (b) does:
>
> A) read XLogCtl->data_checksum_version (while holding insert locks)
> B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
> C) UpdateControlFile()
> 1) write XLOG2_CHECKSUMS with new state
> 2) update XLogCtl->data_checksum_version
> 3) update ControlFile->data_checksum_version
> 4) UpdateControlFile()
> 5) emits barrier
>
> In which case the REDO will have the old state, but the recovery will
> read the XLOG2_CHECKSUMS, and so end up with "on".
>
> This is the root cause of the surprising behavior in TAP 012, I think.
>
> I attempted to trigger these race conditions in TAP 013, but without
> much success. In the end I realized it probably needs more control,
> waiting for the other process to hit the next injection point before
> unpausing the current one. TAP 014 does that, and it shows that with the
> right interleaving of steps the (c) case can end up with both "on" and
> "off" final state.
>
> As I said, I don't claim I fully understand this yet. But I wouldn't
> call this "bug" - AFAICS it won't produce an incorrect final state (I
> haven't seen any such cases).
>
> Still, I wonder if there's a potential issue I failed to notice.
>
I kept thinking about this non-determinism, and I still think I'm right
about the root cause. The checkpointer and datachecksum worker may
interleave in different ways, affecting the final checksum state. The
existing interlock (or lack of of it) is not sufficient.
To verify this hypothesis, I've done a simple thing - I've introduced a
new LWLock, protecting the "critical part" so that the checkpoint_redo
can't happen in between XLogChecksums() and XLogCtl update. Patch
attached, but I don't propose this for commit, I'm not sure if it's
right (or safe), it's merely a PoC to confirm the issue. But it resolves
the weird cases, simply by prohibiting some of the step sequences (so
some of the tests needed to be commented out, as they'd block).
I'm still not sure if it really is an issue or just an annoyance,
because I've not been able to find a case where it'd lead to checksum
failures (or obviously incorrect final state after recovery).
>
> The other question I had when looking at this (concurrency with
> checkpoints) is what we get by doing
>
> MyProc->delayChkptFlags |= DELAY_CHKPT_START;
>
> whenever updating the state in SetDataChecksums... functions. Because
> the only thing that guarantees is the updates happen on one side of the
> checkpoint record. What does that give us, actually?
>
> It does not seem to prevent this surprising behavior, and it does not
> say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO.
>
I still don't understand why this needs DELAY_CHKPT_START ...
I also noticed a couple minor comment issues, per attached patch (this
may need pgindent).
regards
--
Tomas Vondra
Attachments:
[text/x-patch] checksums-extra-lwlock.patch (24.9K, ../../[email protected]/2-checksums-extra-lwlock.patch)
download | inline diff:
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 483e3ea77f5..cb1a77aabb8 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4772,6 +4772,8 @@ SetDataChecksumsOnInProgress(void)
INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-before-xlog", NULL);
+ LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL);
@@ -4781,6 +4783,8 @@ SetDataChecksumsOnInProgress(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockRelease(ChecksumStateLock);
+
elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
@@ -4875,6 +4879,8 @@ SetDataChecksumsOn(void)
INJECTION_POINT_CACHED("datachecksums-enable-checksums-before-xlog", NULL);
+ LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
XLogChecksums(PG_DATA_CHECKSUM_VERSION);
INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL);
@@ -4884,6 +4890,8 @@ SetDataChecksumsOn(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockRelease(ChecksumStateLock);
+
elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_VERSION);
@@ -4980,6 +4988,8 @@ SetDataChecksumsOff(void)
INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-before-xlog", NULL);
+ LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL);
@@ -4989,6 +4999,8 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockRelease(ChecksumStateLock);
+
elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
@@ -5050,6 +5062,8 @@ SetDataChecksumsOff(void)
INJECTION_POINT_CACHED("datachecksums-disable-checksums-before-xlog", NULL);
+ LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
XLogChecksums(PG_DATA_CHECKSUM_OFF);
INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL);
@@ -5059,6 +5073,8 @@ SetDataChecksumsOff(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SpinLockRelease(&XLogCtl->info_lck);
+ LWLockRelease(ChecksumStateLock);
+
elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
data_checksum_version, PG_DATA_CHECKSUM_OFF);
@@ -7771,6 +7787,9 @@ CreateCheckPoint(int flags)
INJECTION_POINT_CACHED("checkpoint-before-redo-checksums", NULL);
+ /* make XLogChecksums + XLogCtl update atomic */
+ LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
WALInsertLockAcquire();
redo_rec.wal_level = wal_level;
SpinLockAcquire(&XLogCtl->info_lck);
@@ -7785,6 +7804,8 @@ CreateCheckPoint(int flags)
XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
+ LWLockRelease(ChecksumStateLock);
+
/*
* XLogInsertRecord will have updated XLogCtl->Insert.RedoRecPtr in
* shared memory and RedoRecPtr in backend-local memory, but we need
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 560659f9568..a9b1e213c7e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ WaitLSN "Waiting to read or update shared Wait-for-LSN state."
LogicalDecodingControl "Waiting to read or update logical decoding status information."
DataChecksumsWorker "Waiting for data checksums worker."
AioWorkerControl "Waiting to update AIO worker information."
+ChecksumState "Waiting to update data checksum state."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index d7eb648bd27..2e566c4cbe3 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN)
PG_LWLOCK(55, LogicalDecodingControl)
PG_LWLOCK(56, DataChecksumsWorker)
PG_LWLOCK(57, AioWorkerControl)
+PG_LWLOCK(58, ChecksumState)
/*
* There also exist several built-in LWLock tranches. As with the predefined
diff --git a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
index 4cea74914d4..c6f7842caa9 100644
--- a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
+++ b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
@@ -245,13 +245,13 @@ sub test_checksum_transition
# concurrent enable + checkpoint, different injection points in the "enable" process
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'on');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on');
@@ -260,12 +260,12 @@ test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-chec
# concurrent disable + checkpoint, different injection points in the "disable" process
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off');
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off');
diff --git a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
index de169f47034..77c34b8b16d 100644
--- a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
+++ b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
@@ -251,13 +251,13 @@ sub test_checksum_transition
# concurrent enable + checkpoint, different injection points in the "enable" process
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'datachecksums-enable-inprogress-checksums-end', 'off');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-before-checkpoint', 'on');
test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'datachecksums-enable-checksums-before-barrier-wait', 'on');
@@ -266,12 +266,12 @@ test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-chec
# concurrent disable + checkpoint, different injection points in the "disable" process
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'datachecksums-disable-checksums-start', 'off');
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-before-checkpoint', 'off');
test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'datachecksums-disable-checksums-before-barrier-wait', 'off');
diff --git a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
index 4a310506482..0c59f6d4095 100644
--- a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
+++ b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
@@ -349,20 +349,20 @@ note('TEST INPROGRESS-ON/5');
datachecksums-enable-inprogress-checksums-after-controlfile);
test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
-note('TEST INPROGRESS-ON/6');
-@steps = qw(create-checkpoint-initial
- checkpoint-before-redo
- checkpoint-before-xlogctl-checksums
- checkpoint-after-xlogctl-checksums
- checkpoint-before-redo-checksums
- datachecksums-enable-inprogress-checksums-before-xlog
- datachecksums-enable-inprogress-checksums-after-xlog
- datachecksums-enable-inprogress-checksums-after-xlogctl
- datachecksums-enable-inprogress-checksums-after-controlfile
- checkpoint-before-redo-wal
- checkpoint-after-redo-wal
- checkpoint-before-old-wal-removal);
-test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+#note('TEST INPROGRESS-ON/6');
+#@steps = qw(create-checkpoint-initial
+# checkpoint-before-redo
+# checkpoint-before-xlogctl-checksums
+# checkpoint-after-xlogctl-checksums
+# checkpoint-before-redo-checksums
+# datachecksums-enable-inprogress-checksums-before-xlog
+# datachecksums-enable-inprogress-checksums-after-xlog
+# datachecksums-enable-inprogress-checksums-after-xlogctl
+# datachecksums-enable-inprogress-checksums-after-controlfile
+# checkpoint-before-redo-wal
+# checkpoint-after-redo-wal
+# checkpoint-before-old-wal-removal);
+#test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
## checksums ON
@@ -395,20 +395,20 @@ note('TEST ON/2');
datachecksums-enable-checksums-after-controlfile);
test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
-note('TEST ON/3');
-@steps = qw(create-checkpoint-initial
- checkpoint-before-redo
- checkpoint-before-xlogctl-checksums
- checkpoint-after-xlogctl-checksums
- checkpoint-before-redo-checksums
- datachecksums-enable-checksums-before-xlog
- datachecksums-enable-checksums-after-xlog
- checkpoint-before-redo-wal
- checkpoint-after-redo-wal
- checkpoint-before-old-wal-removal
- datachecksums-enable-checksums-after-xlogctl
- datachecksums-enable-checksums-after-controlfile);
-test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+#note('TEST ON/3');
+#@steps = qw(create-checkpoint-initial
+# checkpoint-before-redo
+# checkpoint-before-xlogctl-checksums
+# checkpoint-after-xlogctl-checksums
+# checkpoint-before-redo-checksums
+# datachecksums-enable-checksums-before-xlog
+# datachecksums-enable-checksums-after-xlog
+# checkpoint-before-redo-wal
+# checkpoint-after-redo-wal
+# checkpoint-before-old-wal-removal
+# datachecksums-enable-checksums-after-xlogctl
+# datachecksums-enable-checksums-after-controlfile);
+#test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
note('TEST ON/4');
@steps = qw(create-checkpoint-initial
@@ -462,39 +462,39 @@ note('TEST INPROGRESS-OFF/2');
datachecksums-disable-inprogress-checksums-before-barrier-wait);
test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
-note('TEST INPROGRESS-OFF/3');
-@steps = qw(create-checkpoint-initial
- checkpoint-before-redo
- checkpoint-before-xlogctl-checksums
- checkpoint-after-xlogctl-checksums
- checkpoint-before-redo-checksums
- datachecksums-disable-inprogress-checksums-before-xlog
- datachecksums-disable-inprogress-checksums-after-xlog
- checkpoint-before-redo-wal
- checkpoint-after-redo-wal
- checkpoint-before-old-wal-removal
- datachecksums-disable-inprogress-checksums-after-xlogctl
- datachecksums-disable-inprogress-checksums-after-controlfile
- datachecksums-disable-inprogress-checksums-before-checkpoint
- datachecksums-disable-inprogress-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
-
-note('TEST INPROGRESS-OFF/4');
-@steps = qw(create-checkpoint-initial
- checkpoint-before-redo
- checkpoint-before-xlogctl-checksums
- checkpoint-after-xlogctl-checksums
- checkpoint-before-redo-checksums
- datachecksums-disable-inprogress-checksums-before-xlog
- datachecksums-disable-inprogress-checksums-after-xlog
- datachecksums-disable-inprogress-checksums-after-xlogctl
- datachecksums-disable-inprogress-checksums-after-controlfile
- datachecksums-disable-inprogress-checksums-before-checkpoint
- checkpoint-before-redo-wal
- checkpoint-after-redo-wal
- checkpoint-before-old-wal-removal
- datachecksums-disable-inprogress-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+#note('TEST INPROGRESS-OFF/3');
+#@steps = qw(create-checkpoint-initial
+# checkpoint-before-redo
+# checkpoint-before-xlogctl-checksums
+# checkpoint-after-xlogctl-checksums
+# checkpoint-before-redo-checksums
+# datachecksums-disable-inprogress-checksums-before-xlog
+# datachecksums-disable-inprogress-checksums-after-xlog
+# checkpoint-before-redo-wal
+# checkpoint-after-redo-wal
+# checkpoint-before-old-wal-removal
+# datachecksums-disable-inprogress-checksums-after-xlogctl
+# datachecksums-disable-inprogress-checksums-after-controlfile
+# datachecksums-disable-inprogress-checksums-before-checkpoint
+# datachecksums-disable-inprogress-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+#note('TEST INPROGRESS-OFF/4');
+#@steps = qw(create-checkpoint-initial
+# checkpoint-before-redo
+# checkpoint-before-xlogctl-checksums
+# checkpoint-after-xlogctl-checksums
+# checkpoint-before-redo-checksums
+# datachecksums-disable-inprogress-checksums-before-xlog
+# datachecksums-disable-inprogress-checksums-after-xlog
+# datachecksums-disable-inprogress-checksums-after-xlogctl
+# datachecksums-disable-inprogress-checksums-after-controlfile
+# datachecksums-disable-inprogress-checksums-before-checkpoint
+# checkpoint-before-redo-wal
+# checkpoint-after-redo-wal
+# checkpoint-before-old-wal-removal
+# datachecksums-disable-inprogress-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
## checksums OFF
@@ -532,22 +532,22 @@ note('TEST OFF/2');
datachecksums-disable-checksums-before-barrier-wait);
test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
-note('TEST OFF/3');
-@steps = qw(create-checkpoint-initial
- checkpoint-before-redo
- checkpoint-before-xlogctl-checksums
- checkpoint-after-xlogctl-checksums
- checkpoint-before-redo-checksums
- datachecksums-disable-checksums-before-xlog
- datachecksums-disable-checksums-after-xlog
- checkpoint-before-redo-wal
- checkpoint-after-redo-wal
- datachecksums-disable-checksums-after-xlogctl
- datachecksums-disable-checksums-after-controlfile
- datachecksums-disable-checksums-before-checkpoint
- checkpoint-before-old-wal-removal
- datachecksums-disable-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+#note('TEST OFF/3');
+#@steps = qw(create-checkpoint-initial
+# checkpoint-before-redo
+# checkpoint-before-xlogctl-checksums
+# checkpoint-after-xlogctl-checksums
+# checkpoint-before-redo-checksums
+# datachecksums-disable-checksums-before-xlog
+# datachecksums-disable-checksums-after-xlog
+# checkpoint-before-redo-wal
+# checkpoint-after-redo-wal
+# datachecksums-disable-checksums-after-xlogctl
+# datachecksums-disable-checksums-after-controlfile
+# datachecksums-disable-checksums-before-checkpoint
+# checkpoint-before-old-wal-removal
+# datachecksums-disable-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
note('TEST OFF/4');
@steps = qw(create-checkpoint-initial
@@ -566,22 +566,22 @@ note('TEST OFF/4');
datachecksums-disable-checksums-before-barrier-wait);
test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
-note('TEST OFF/5');
-@steps = qw(create-checkpoint-initial
- checkpoint-before-redo
- checkpoint-before-xlogctl-checksums
- checkpoint-after-xlogctl-checksums
- checkpoint-before-redo-checksums
- datachecksums-disable-checksums-before-xlog
- datachecksums-disable-checksums-after-xlog
- checkpoint-before-redo-wal
- checkpoint-after-redo-wal
- datachecksums-disable-checksums-after-xlogctl
- datachecksums-disable-checksums-after-controlfile
- datachecksums-disable-checksums-before-checkpoint
- checkpoint-before-old-wal-removal
- datachecksums-disable-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+#note('TEST OFF/5');
+#@steps = qw(create-checkpoint-initial
+# checkpoint-before-redo
+# checkpoint-before-xlogctl-checksums
+# checkpoint-after-xlogctl-checksums
+# checkpoint-before-redo-checksums
+# datachecksums-disable-checksums-before-xlog
+# datachecksums-disable-checksums-after-xlog
+# checkpoint-before-redo-wal
+# checkpoint-after-redo-wal
+# datachecksums-disable-checksums-after-xlogctl
+# datachecksums-disable-checksums-after-controlfile
+# datachecksums-disable-checksums-before-checkpoint
+# checkpoint-before-old-wal-removal
+# datachecksums-disable-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
note('TEST OFF/6');
@steps = qw(create-checkpoint-initial
[text/x-patch] comment-fixes.patch (6.5K, ../../[email protected]/3-comment-fixes.patch)
download | inline diff:
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fecdf0d4b05..462414323a1 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4807,9 +4807,8 @@ SetDataChecksumsOn(void)
/*
* The only allowed state transition to "on" is from "inprogress-on" since
- * that state ensures that all pages will have data checksums written. No
- * such state transition exists, if it does happen it's likely due to a
- * programmer error.
+ * that state ensures that all pages will have data checksums written. Any
+ * other attempted state transition is likely due to a programmer error.
*/
if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
{
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 9803a0ee2a1..ad4bf4bd2a8 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -88,7 +88,7 @@ AuxiliaryProcessMainCommon(void)
*
* The postmaster (which is what gets forked into the new child process)
* does not handle barriers, therefore it may not have the current value
- * of LocalDataChecksumVersion value (it'll have the value read from the
+ * of LocalDataChecksumState value (it'll have the value read from the
* control file, which may be arbitrarily old).
*
* NB: Even if the postmaster handled barriers, the value might still be
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 33430147ff2..520eb8db16b 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -20,8 +20,8 @@
* When enabling checksums in an online cluster, data_checksums will be set to
* "inprogress-on" which signals that write operations MUST compute and write
* the checksum on the data page, but during reading the checksum SHALL NOT be
- * verified. This ensures that all objects created during when checksums are
- * being enabled will have checksums set, but reads won't fail due to missing or
+ * verified. This ensures that all objects created while checksums are being
+ * enabled will have checksums set, but reads won't fail due to missing or
* invalid checksums. Invalid checksums can be present in case the cluster had
* checksums enabled, then disabled them and updated the page while they were
* disabled.
@@ -299,10 +299,11 @@ typedef struct DataChecksumsStateStruct
bool launcher_running;
/*
- * Is a worker process currently running? This is set by the worker
- * launcher when it starts waiting for a worker process to finish.
+ * PID of the worker process, if it's currently running, of InvalidPid
+ * if none. This is set by the worker launcher when it starts waiting
+ * for a worker process to finish.
*/
- int worker_pid;
+ pid_t worker_pid;
/*
* These fields indicate the target state that the launcher is currently
@@ -320,12 +321,8 @@ typedef struct DataChecksumsStateStruct
int cost_limit;
/*
- * Signaling between the launcher and the worker process.
- *
- * As there is only a single worker, and the launcher won't read these
- * until the worker exits, they can be accessed without the need for a
- * lock. If multiple workers are supported then this will have to be
- * revisited.
+ * Signaling between the launcher and the worker process. Protected by
+ * DataChecksumsWorkerLock.
*/
/* result, set by worker before exiting */
@@ -599,9 +596,9 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
*
* If the launcher is currently busy enabling the checksums, and we want
* them disabled (or vice versa), the launcher will notice that at latest
- * when it's about to exit, and will loop back process the new request. So
- * if the launcher is already running, we don't need to do anything more
- * here to abort it.
+ * when it's about to exit, and will loop back to process the new request.
+ * So if the launcher is already running, we don't need to do anything
+ * more here to abort it.
*
* If you call pg_enable/disable_data_checksums() twice in a row, before
* the launcher has had a chance to start up, we still end up launching it
@@ -710,10 +707,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
UnlockReleaseBuffer(buf);
- /*
- * This is the only place where we check if we are asked to abort, the
- * abortion will bubble up from here.
- */
+ /* Check if we are asked to abort, the abortion will bubble up. */
Assert(operation == ENABLE_DATACHECKSUMS);
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
@@ -924,8 +918,8 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
* performed checksum operations exits. A launcher process which is exiting due
* to a duplicate started launcher does not need to perform any cleanup and
* this function should not be called. Otherwise, we need to clean up the abort
- * flag to ensure that processing started again if it was previously aborted
- * (note: started again, *not* restarted from where it left off).
+ * flag to ensure that processing can be started again if it was previously
+ * aborted (note: started again, *not* restarted from where it left off).
*/
static void
launcher_exit(int code, Datum arg)
@@ -1434,7 +1428,7 @@ FreeDatabaseList(List *dblist)
* BuildRelationList
* Compile a list of relations in the database
*
- * Returns a list of OIDs for the request relation types. If temp_relations
+ * Returns a list of OIDs for the requested relation types. If temp_relations
* is True then only temporary relations are returned. If temp_relations is
* False then non-temporary relations which have data checksums are returned.
* If include_shared is True then shared relations are included as well in a
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 2460e550f96..c1457eb34f0 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -773,7 +773,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
*
* The postmaster (which is what gets forked into the new child process)
* does not handle barriers, therefore it may not have the current value
- * of LocalDataChecksumVersion value (it'll have the value read from the
+ * of LocalDataChecksumState value (it'll have the value read from the
* control file, which may be arbitrarily old).
*
* NB: Even if the postmaster handled barriers, the value might still be
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-28 11:28 Daniel Gustafsson <[email protected]>
parent: Tomas Vondra <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-05-28 11:28 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote:
> I suppose this means we should not be updating the checksum state
> without emitting the barrier? I think all other places do that.
Good catch, it's indeed a bug, any state change must emit a procsignalbarrier
to maintain cluster consistency. I ended up writing a test for this very case
as well.
> I'm still not sure if it really is an issue or just an annoyance,
> because I've not been able to find a case where it'd lead to checksum
> failures (or obviously incorrect final state after recovery).
I've tried to get it to reach an incorrect end state but failed, but I do agree
that maybe we need an improved locking protocol around state updates. Need to
spend some more time thinking about this.
> I still don't understand why this needs DELAY_CHKPT_START ...
Having stared at this for some time, and going over old threads, I think this
is a mistake. AFAICT though it cannot cause any error, so I'd lean towards
erring on the safe side by leaving as is and looking at removing in 20. What
do you think?
> I also noticed a couple minor comment issues, per attached patch (this
> may need pgindent).
I ended up splitting this into two, one for the comment fixes and one for the
data type change.
I propose applying the three patches below to v19 to fix the promotion issue
before we wrap beta1.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] 0003-Use-correct-datatype-for-PID.patch (1.2K, ../../[email protected]/2-0003-Use-correct-datatype-for-PID.patch)
download | inline diff:
From aec5f239b2b09fd877faebb684cd6885b165bd62 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 28 May 2026 12:05:31 +0200
Subject: [PATCH 3/3] Use correct datatype for PID
The datachecksums launcher was using int for storing a PID, but
the correct type is pid_t (which is defined to be a signed int).
Author: Tomas Vondra <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/postmaster/datachecksum_state.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index b61674ae957..b578a7cc70e 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -303,7 +303,7 @@ typedef struct DataChecksumsStateStruct
* none. This is set by the worker launcher when it starts waiting for a
* worker process to finish.
*/
- int worker_pid;
+ pid_t worker_pid;
/*
* These fields indicate the target state that the launcher is currently
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0002-Improve-comments-in-online-checksums-code.patch (7.1K, ../../[email protected]/3-0002-Improve-comments-in-online-checksums-code.patch)
download | inline diff:
From 19d6bc95cf6b8becaa75dbca9b0fdf1d6ce2aef1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 28 May 2026 12:02:06 +0200
Subject: [PATCH 2/3] Improve comments in online checksums code
Spelling fixes and rewording outdated information.
Author: Tomas Vondra <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/access/transam/xlog.c | 5 ++-
src/backend/postmaster/auxprocess.c | 2 +-
src/backend/postmaster/datachecksum_state.c | 34 +++++++++------------
src/backend/utils/init/postinit.c | 2 +-
4 files changed, 18 insertions(+), 25 deletions(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b32f54f8402..d69d03b2ef3 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4807,9 +4807,8 @@ SetDataChecksumsOn(void)
/*
* The only allowed state transition to "on" is from "inprogress-on" since
- * that state ensures that all pages will have data checksums written. No
- * such state transition exists, if it does happen it's likely due to a
- * programmer error.
+ * that state ensures that all pages will have data checksums written. Any
+ * other attempted state transition is likely due to a programmer error.
*/
if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
{
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 9803a0ee2a1..ad4bf4bd2a8 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -88,7 +88,7 @@ AuxiliaryProcessMainCommon(void)
*
* The postmaster (which is what gets forked into the new child process)
* does not handle barriers, therefore it may not have the current value
- * of LocalDataChecksumVersion value (it'll have the value read from the
+ * of LocalDataChecksumState value (it'll have the value read from the
* control file, which may be arbitrarily old).
*
* NB: Even if the postmaster handled barriers, the value might still be
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 33430147ff2..b61674ae957 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -20,8 +20,8 @@
* When enabling checksums in an online cluster, data_checksums will be set to
* "inprogress-on" which signals that write operations MUST compute and write
* the checksum on the data page, but during reading the checksum SHALL NOT be
- * verified. This ensures that all objects created during when checksums are
- * being enabled will have checksums set, but reads won't fail due to missing or
+ * verified. This ensures that all objects created while checksums are being
+ * enabled will have checksums set, but reads won't fail due to missing or
* invalid checksums. Invalid checksums can be present in case the cluster had
* checksums enabled, then disabled them and updated the page while they were
* disabled.
@@ -299,8 +299,9 @@ typedef struct DataChecksumsStateStruct
bool launcher_running;
/*
- * Is a worker process currently running? This is set by the worker
- * launcher when it starts waiting for a worker process to finish.
+ * PID of the worker process, if it's currently running, of InvalidPid if
+ * none. This is set by the worker launcher when it starts waiting for a
+ * worker process to finish.
*/
int worker_pid;
@@ -320,12 +321,8 @@ typedef struct DataChecksumsStateStruct
int cost_limit;
/*
- * Signaling between the launcher and the worker process.
- *
- * As there is only a single worker, and the launcher won't read these
- * until the worker exits, they can be accessed without the need for a
- * lock. If multiple workers are supported then this will have to be
- * revisited.
+ * Signaling between the launcher and the worker process. Protected by
+ * DataChecksumsWorkerLock.
*/
/* result, set by worker before exiting */
@@ -599,9 +596,9 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
*
* If the launcher is currently busy enabling the checksums, and we want
* them disabled (or vice versa), the launcher will notice that at latest
- * when it's about to exit, and will loop back process the new request. So
- * if the launcher is already running, we don't need to do anything more
- * here to abort it.
+ * when it's about to exit, and will loop back to process the new request.
+ * So if the launcher is already running, we don't need to do anything
+ * more here to abort it.
*
* If you call pg_enable/disable_data_checksums() twice in a row, before
* the launcher has had a chance to start up, we still end up launching it
@@ -710,10 +707,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
UnlockReleaseBuffer(buf);
- /*
- * This is the only place where we check if we are asked to abort, the
- * abortion will bubble up from here.
- */
+ /* Check if we are asked to abort, the abortion will bubble up. */
Assert(operation == ENABLE_DATACHECKSUMS);
LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
@@ -924,8 +918,8 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
* performed checksum operations exits. A launcher process which is exiting due
* to a duplicate started launcher does not need to perform any cleanup and
* this function should not be called. Otherwise, we need to clean up the abort
- * flag to ensure that processing started again if it was previously aborted
- * (note: started again, *not* restarted from where it left off).
+ * flag to ensure that processing can be started again if it was previously
+ * aborted (note: started again, *not* restarted from where it left off).
*/
static void
launcher_exit(int code, Datum arg)
@@ -1434,7 +1428,7 @@ FreeDatabaseList(List *dblist)
* BuildRelationList
* Compile a list of relations in the database
*
- * Returns a list of OIDs for the request relation types. If temp_relations
+ * Returns a list of OIDs for the requested relation types. If temp_relations
* is True then only temporary relations are returned. If temp_relations is
* False then non-temporary relations which have data checksums are returned.
* If include_shared is True then shared relations are included as well in a
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 2460e550f96..c1457eb34f0 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -773,7 +773,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
*
* The postmaster (which is what gets forked into the new child process)
* does not handle barriers, therefore it may not have the current value
- * of LocalDataChecksumVersion value (it'll have the value read from the
+ * of LocalDataChecksumState value (it'll have the value read from the
* control file, which may be arbitrarily old).
*
* NB: Even if the postmaster handled barriers, the value might still be
--
2.39.3 (Apple Git-146)
[application/octet-stream] 0001-Fix-checksum-state-transition-during-promotion.patch (5.7K, ../../[email protected]/4-0001-Fix-checksum-state-transition-during-promotion.patch)
download | inline diff:
From 37ada0f792137ce33c0346645e792a3e85158a6d Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 28 May 2026 11:45:36 +0200
Subject: [PATCH 1/3] Fix checksum state transition during promotion
When a standby is promoted to primary during checksum enabling when the
state is inprogress-on, the standby shall revert the state to off since
checksums weren't fully enabled at the time of the crash. Consider the
following scenario:
1) primary/standby cluster has checksums off
2) primary starts enabling checksums
3) primary moves to inprogress-on
4) standby receives that and moves to inprogress-on too
5) primary crashes
6) standby gets promoted, and does the StartupXLOG thing
7) standby moves from inprogress-on back to off
Any processes in the standby need to be informed at step 6 to change
state with a procsignalbarrier, else they will stay in inprogress-on
while new backends will see the state as off. StartupXLOG failed to
emit a procsignalbarrier which caused inconsistent state in the node
promoted to primary.
Fixed by emitting a procsignalbarrier during promotion, and adding a
new test for this scenario.
Author: Daniel Gustafsson <[email protected]>
Reported-by: Tomas Vondra <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/access/transam/xlog.c | 5 +-
.../test_checksums/t/003_standby_restarts.pl | 58 +++++++++++++++++++
2 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fecdf0d4b05..b32f54f8402 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -6610,6 +6610,7 @@ StartupXLOG(void)
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
+ EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF);
ereport(WARNING,
errmsg("enabling data checksums was interrupted"),
errhint("Data checksum processing must be manually restarted for checksums to be enabled."));
@@ -6621,7 +6622,7 @@ StartupXLOG(void)
* checksums and we can move to off instead of prompting the user to
* perform any action.
*/
- if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
+ else if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
{
XLogChecksums(PG_DATA_CHECKSUM_OFF);
@@ -6629,6 +6630,8 @@ StartupXLOG(void)
XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
SetLocalDataChecksumState(XLogCtl->data_checksum_version);
SpinLockRelease(&XLogCtl->info_lck);
+
+ EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF);
}
/*
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index 5bbf38ed21c..b1f8eedb0bf 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -226,4 +226,62 @@ unlike(
qr/page verification failed,.+\d$/m,
"no checksum validation errors in standby log");
+# ---------------------------------------------------------------------------
+# Test that enforced state transitions during promotion (via StartupXLOG) are
+# performed as expected. When the primary crashes during inprogress-on the
+# standby should revert to off at promotion. In order to check the transition
+# the test will keep an open psql session with the standby during promotion.
+
+# The cluster is currently broken down from the previous test. Start up the
+# primary as primary, disable checksums and create a new standby from that
+# state.
+$node_standby->clean_node();
+$node_primary->start();
+disable_data_checksums($node_primary, wait => 'off');
+
+# Re-create a new streaming standby linking to primary. The replication slot
+# name is reused from earlier but a fresh backup is taken
+$backup_name = 'my_new_backup';
+$node_primary->backup($backup_name);
+$node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby');
+$node_standby->init_from_backup($node_primary, $backup_name,
+ has_streaming => 1);
+$node_standby->append_conf(
+ 'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby->start;
+$node_primary->wait_for_catchup($node_standby, 'replay');
+
+# Open a background psql connection on the primary and inject a barrier to
+# block progress on to keep the state from advancing past inprogress-on
+my $node_primary_bpsql = $node_primary->background_psql('postgres');
+$node_primary_bpsql->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+# Also open a background psql connection to the standby to make sure we have
+# an active backend during promotion.
+my $node_standby_bpsql = $node_standby->background_psql('postgres');
+
+# Start to enable checksums and wait until both primary and standby have moved
+# to the inprogress-on state. Processing will block here as the temporary rel
+# barrier will block the primary from finishing.
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+$node_primary->wait_for_catchup($node_standby, 'replay');
+test_checksum_state($node_standby, 'inprogress-on');
+
+# Crash the primary before checksums are enabled and promote the standby. The
+# new primary node will now revert the state of 'off' since checksums weren't
+# fully enabled during the crash.
+$node_primary->teardown_node();
+$node_standby->promote;
+wait_for_checksum_state($node_standby, 'off');
+
+# Ensure that the any backend which was active befire, and during, promotion
+# sees the new state.
+$result = $node_standby_bpsql->query_safe("SHOW data_checksums;");
+is($result, 'off',
+ 'ensure checksums are set to off after promotion during inprogress-on');
+
+$node_standby_bpsql->quit;
+$node_standby->stop;
+
done_testing();
--
2.39.3 (Apple Git-146)
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-28 11:51 Tomas Vondra <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Tomas Vondra @ 2026-05-28 11:51 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
On 5/28/26 13:28, Daniel Gustafsson wrote:
>> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote:
>
>> I suppose this means we should not be updating the checksum state
>> without emitting the barrier? I think all other places do that.
>
> Good catch, it's indeed a bug, any state change must emit a procsignalbarrier
> to maintain cluster consistency. I ended up writing a test for this very case
> as well.
>
Good.
>> I'm still not sure if it really is an issue or just an annoyance,
>> because I've not been able to find a case where it'd lead to checksum
>> failures (or obviously incorrect final state after recovery).
>
> I've tried to get it to reach an incorrect end state but failed, but I do agree
> that maybe we need an improved locking protocol around state updates. Need to
> spend some more time thinking about this.
>
OK
>> I still don't understand why this needs DELAY_CHKPT_START ...
>
> Having stared at this for some time, and going over old threads, I think this
> is a mistake. AFAICT though it cannot cause any error, so I'd lean towards
> erring on the safe side by leaving as is and looking at removing in 20. What
> do you think?
>
I'd probably try to fix this for 19, otherwise it may be confusing
people looking at the code in the future. We're still months from 19
getting released. Ofc, maybe I'm underestimating the risk.
>> I also noticed a couple minor comment issues, per attached patch (this
>> may need pgindent).
>
> I ended up splitting this into two, one for the comment fixes and one for the
> data type change.
>
> I propose applying the three patches below to v19 to fix the promotion issue
> before we wrap beta1.
>
WFM
> --
> Daniel Gustafsson
>
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-29 20:08 Daniel Gustafsson <[email protected]>
parent: Tomas Vondra <[email protected]>
0 siblings, 1 reply; 60+ messages in thread
From: Daniel Gustafsson @ 2026-05-29 20:08 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
> On 28 May 2026, at 13:51, Tomas Vondra <[email protected]> wrote:
>
> On 5/28/26 13:28, Daniel Gustafsson wrote:
>>> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote:
>>
>>> I suppose this means we should not be updating the checksum state
>>> without emitting the barrier? I think all other places do that.
>>
>> Good catch, it's indeed a bug, any state change must emit a procsignalbarrier
>> to maintain cluster consistency. I ended up writing a test for this very case
>> as well.
>
> Good.
I've pushed this now, along with your other findings, ahead of the beta1
deadline, buildfarm seems happy so far.
>>> I still don't understand why this needs DELAY_CHKPT_START ...
>>
>> Having stared at this for some time, and going over old threads, I think this
>> is a mistake. AFAICT though it cannot cause any error, so I'd lean towards
>> erring on the safe side by leaving as is and looking at removing in 20. What
>> do you think?
>>
>
> I'd probably try to fix this for 19, otherwise it may be confusing
> people looking at the code in the future. We're still months from 19
> getting released. Ofc, maybe I'm underestimating the risk.
You're probably right. Once beta1 is out I'll work on getting this fixed.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-05-29 20:27 Tomas Vondra <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 0 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-05-29 20:27 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]>
On 5/29/26 22:08, Daniel Gustafsson wrote:
>> On 28 May 2026, at 13:51, Tomas Vondra <[email protected]> wrote:
>>
>> On 5/28/26 13:28, Daniel Gustafsson wrote:
>>>> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote:
>>>
>>>> I suppose this means we should not be updating the checksum state
>>>> without emitting the barrier? I think all other places do that.
>>>
>>> Good catch, it's indeed a bug, any state change must emit a procsignalbarrier
>>> to maintain cluster consistency. I ended up writing a test for this very case
>>> as well.
>>
>> Good.
>
> I've pushed this now, along with your other findings, ahead of the beta1
> deadline, buildfarm seems happy so far.
>
Thanks!
>>>> I still don't understand why this needs DELAY_CHKPT_START ...
>>>
>>> Having stared at this for some time, and going over old threads, I think this
>>> is a mistake. AFAICT though it cannot cause any error, so I'd lean towards
>>> erring on the safe side by leaving as is and looking at removing in 20. What
>>> do you think?
>>>
>>
>> I'd probably try to fix this for 19, otherwise it may be confusing
>> people looking at the code in the future. We're still months from 19
>> getting released. Ofc, maybe I'm underestimating the risk.
>
> You're probably right. Once beta1 is out I'll work on getting this fixed.
>
+1
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-07-02 05:06 Palak Chaturvedi <[email protected]>
parent: Tomas Vondra <[email protected]>
2 siblings, 1 reply; 60+ messages in thread
From: Palak Chaturvedi @ 2026-07-02 05:06 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; pgsql-hackers
Hi Tomas,
Thanks for these test patches. They are quite useful. I've been going
through them because we think they'll also help with the TAP tests for the
online shared_buffers resize work , where I'm ending up reusing several of
the same helpers (attach_injection_points, background_rw_pgbench, and a
couple of others).
Would you be open to lifting these helpers out into a shared Perl module
(for example under src/test/perl/PostgreSQL/Test/…) so both test suites can
consume them instead of each copy-pasting? I'm happy to prepare a small
prep patch on top of your latest version if that sounds reasonable to you.
Thanks,
Palak
On Mon, 4 May 2026 at 18:47, Tomas Vondra <[email protected]> wrote:
> Hi,
>
> Thanks for getting this feature pushed, and for resolving the failures
> reported since the feature freeze. I consider this to be an important
> improvement, not just for the feature itself, but also because of all
> the useful infrastructure it added.
>
> Attached is a refined version of the TAP tests already posted by Daniel
> some time ago [1]. Unfortunately, that .txt did not apply cleanly for
> some reason, so here's a better version.
>
> I found these tests quite useful when reasoning about how the patch
> behaves in concurrent environment (e.g. with multiple sessions
> triggering checksum enable/disable, or with a checkpoint, crashes, etc).
>
> At this point all the tests pass, but there are a couple cases with
> correct but slightly surprising behavior, worth discussing. Which is
> what this e-mail is going to be about.
>
> I'll explain what the TAP tests aim to do first, and then discuss the
> slightly surprising behavior.
>
> It's not meant for inclusion into PG19, at least not in this shape - I
> wrote those TAP tests while investigating some of the earlier failures
> and/or when wondering about behavior in various situations (sequence of
> concurrent steps, race conditions, ...). So it's more of an exhaustive,
> and the tests are somewhat redundant (N+1 is often just (N + some small
> tweak)).
>
> I can imagine distilling it into a tiny subset, and adding that. But
> that's up to discussion. But that's for later.
>
>
> Let me briefly explain what the various TAP tests aim to do. From the
> very beginning, my main concern regarding this patch was race conditions
> when updating the shared state about effective data_checksum_version.
> Because the state is effectively split into about three or four places:
>
> * LocalDataChecksumVersion (local cache)
> * XLogCtl->data_checksum_version (XLogCtl->info_lck)
> * ControlFile->data_checksum_version (ControlFileLock)
> * state in control file on disk
>
> These pieces are protected by different locks, the protocol for updating
> and/or reading the various flags is not trivial (and some of the fixed
> issues were due to ControlFile->data_checksum_version being updated from
> a place that shouldn't have).
>
> So the primary goal of the TAP tests was to check for race conditions by
> leveraging injection points to step through concurrent processes in a
> deterministic way. The first couple patches (0001-0004) add debug
> logging and injection points into a lot of places. And by "a lot" I mean
> ~80 new injection points, which is about the number of injection points
> we have in master now. Anyway, this allows stepping through concurrent
> checksum changes, and also checksum change vs. checkpointer.
>
> Then come the actual TAP tests:
>
> 1) 0005-TAP-10-concurrent-checksum-changes.patch
>
> Two concurrent checksum changes. The first one gets paused at an
> injection point, then the second one gets initiated.
>
> 2) 0006-TAP-11-concurrency-with-checkpoints.patch
>
> A checksum change + checkpoint. The change gets paused at an injection
> point, a synchronous checkpoint is performed.
>
> 3) 0007-TAP-12-crashes-at-injection-points.patch
>
> Similar to 0006, but with a crash + recovery. A checksum change gets
> paused at an injection point, a synchronous checkpoint is performed. The
> changes gets wpken up and either completes, or pauses on a different
> injection point. A restart/crash happens.
>
> 4) 0008-TAP-13-concurrency-with-checkpoint-REDO.patch
>
> Similar to 0007, but the checkpoint is not synchronous - happens in the
> background, so that the TAP can step through both sides and interleave
> them in an arbitrary way. This matters because the checksum change
> updates the different state pieces (XlogCtl/ControlFile), while the
> checkpointer reads them to record initial state for REDO etc.
>
> 5) 0009-TAP-14-checkpoints-with-crashes.patch
>
> Similar to 0008, except that the steps are more fine grained, and
> focused on two particular cases with surprisingly different final state.
>
>
> AFAIK everything works as expected, except for two cases in the "TAP
> 012" test. One for the "enabling" direction, one for the "disabling"
> direction. I'm going to discuss the "enabling" direction, I believe the
> other case is just a mirror with the same root cause.
>
> The TAP 012 tests checksum change with a concurrent checkpoint, followed
> by a crash, and tests the final state. It pauses the change at an
> injection point, does a checkpoint, proceeds to the next injection
> point, crashes and does recovery. The expectation is that the final
> state "flips" at some injection point, once it gets further enough, and
> stays there. But what actually happens is this:
>
> a) test_checksum_transition(
> 'disabled', 'enable', undef,
> 'datachecksums-enable-inprogress-checksums-end',
> 'datachecksums-enable-checksums-start',
> 'off');
>
> b) test_checksum_transition(
> 'disabled', 'enable', undef,
> 'datachecksums-enable-checksums-start',
> 'datachecksums-enable-checksums-after-xlog',
> 'on');
>
> c) test_checksum_transition(
> 'disabled', 'enable', 'datachecksums-enable-checksums-start',
> 'datachecksums-enable-checksums-after-xlogs',
> 'datachecksums-enable-checksums-after-xlogctl',
> 'off');
>
> This says that if the checkpoint happens after
> 'datachecksums-enable-inprogress-checksums-end' or after
> 'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e.
> enabling checksums fails).
>
> But if the checkpoint happens after
> 'datachecksums-enable-checksums-start', we end up with "on" (after
> recovery).
>
> This is a bit surprising, because that injection point is before
> 'datachecksums-enable-checksums-after-xlog'. So the enabling process
> gets further and further, but the final state flips off -> on -> off,
> contradicting the expectation that it changes once.
>
> I haven't quite wrapped my head around it yet, but my understanding is
> this is due to a race condition between the checksum launcher (writing
> XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer
> (reading the shmem state and generating REDO).
>
> The launcher does this sequence of steps:
>
> 1) write XLOG2_CHECKSUMS with new state
> 2) update XLogCtl->data_checksum_version
> 3) update ControlFile->data_checksum_version
> 4) UpdateControlFile()
> 5) emits barrier
>
> while the checkpointer (CreateCheckPoint) does this:
>
> A) read XLogCtl->data_checksum_version (while holding insert locks)
> B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
> C) UpdateControlFile()
>
> The outcome depends on how exactly these two sequences interleave. For
> example, this can happen:
>
> 1) write XLOG2_CHECKSUMS with new state
> A) read XLogCtl->data_checksum_version (while holding insert locks)
> B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
> C) UpdateControlFile()
> 2) update XLogCtl->data_checksum_version
> 3) update ControlFile->data_checksum_version
> 4) UpdateControlFile()
> 5) emits barrier
>
> Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and
> so redo won't see it), but the checkpoint will still get the old
> checksum state from XLogCtl. And so the outcome is "off", per case (c).
>
> But it can also happen what case (b) does:
>
> A) read XLogCtl->data_checksum_version (while holding insert locks)
> B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
> C) UpdateControlFile()
> 1) write XLOG2_CHECKSUMS with new state
> 2) update XLogCtl->data_checksum_version
> 3) update ControlFile->data_checksum_version
> 4) UpdateControlFile()
> 5) emits barrier
>
> In which case the REDO will have the old state, but the recovery will
> read the XLOG2_CHECKSUMS, and so end up with "on".
>
> This is the root cause of the surprising behavior in TAP 012, I think.
>
> I attempted to trigger these race conditions in TAP 013, but without
> much success. In the end I realized it probably needs more control,
> waiting for the other process to hit the next injection point before
> unpausing the current one. TAP 014 does that, and it shows that with the
> right interleaving of steps the (c) case can end up with both "on" and
> "off" final state.
>
> As I said, I don't claim I fully understand this yet. But I wouldn't
> call this "bug" - AFAICS it won't produce an incorrect final state (I
> haven't seen any such cases).
>
> Still, I wonder if there's a potential issue I failed to notice.
>
>
> The other question I had when looking at this (concurrency with
> checkpoints) is what we get by doing
>
> MyProc->delayChkptFlags |= DELAY_CHKPT_START;
>
> whenever updating the state in SetDataChecksums... functions. Because
> the only thing that guarantees is the updates happen on one side of the
> checkpoint record. What does that give us, actually?
>
> It does not seem to prevent this surprising behavior, and it does not
> say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO.
>
>
> regards
>
> [1]
>
> https://www.postgresql.org/message-id/9197F930-DDEB-4CAC-82A2-16FEC715CCE8%40yesql.se
>
> --
> Tomas Vondra
>
^ permalink raw reply [nested|flat] 60+ messages in thread
* Re: Changing the state of data checksums in a running cluster
@ 2026-07-02 08:29 Tomas Vondra <[email protected]>
parent: Palak Chaturvedi <[email protected]>
0 siblings, 0 replies; 60+ messages in thread
From: Tomas Vondra @ 2026-07-02 08:29 UTC (permalink / raw)
To: Palak Chaturvedi <[email protected]>; pgsql-hackers
On 7/2/26 07:06, Palak Chaturvedi wrote:
> Hi Tomas,
>
> Thanks for these test patches. They are quite useful. I've been going
> through them because we think they'll also help with the TAP tests for
> the online shared_buffers resize work , where I'm ending up reusing
> several of the same helpers (attach_injection_points,
> background_rw_pgbench, and a couple of others).
> Would you be open to lifting these helpers out into a shared Perl module
> (for example under src/test/perl/PostgreSQL/Test/…) so both test suites
> can consume them instead of each copy-pasting? I'm happy to prepare a
> small prep patch on top of your latest version if that sounds reasonable
> to you.
>
I suppose you mean the functions for managing injection points (waiting,
waking, attachin/detaching, ...)? Yes, it might be useful to have that
in a Perl module, so feel free to create one.
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 60+ messages in thread
end of thread, other threads:[~2026-07-02 08:29 UTC | newest]
Thread overview: 60+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2021-02-06 21:13 [PATCH 08/18] *an exclusive Justin Pryzby <[email protected]>
2026-02-05 23:15 Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-02-06 17:15 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-03-11 23:56 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-03-15 22:47 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-03-15 22:59 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-03-17 22:01 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-03-19 13:49 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-03-19 23:34 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-03-27 23:30 ` Re: Changing the state of data checksums in a running cluster Heikki Linnakangas <[email protected]>
2026-03-30 14:43 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-01 23:01 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-02 09:27 ` Re: Changing the state of data checksums in a running cluster Heikki Linnakangas <[email protected]>
2026-04-02 18:48 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-03 15:33 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-03 16:00 ` Re: Changing the state of data checksums in a running cluster Heikki Linnakangas <[email protected]>
2026-04-03 17:14 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-03 17:56 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-03 19:29 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-03 21:46 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-03 22:59 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-04 00:35 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-04 22:27 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-05 04:56 ` Re: Changing the state of data checksums in a running cluster Andres Freund <[email protected]>
2026-04-05 14:36 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-06 00:20 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-06 17:00 ` Re: Changing the state of data checksums in a running cluster Alexander Lakhin <[email protected]>
2026-04-06 17:34 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-08 07:31 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-04-08 20:10 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-23 10:16 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-25 11:59 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]>
2026-04-28 22:06 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-28 20:25 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-04-29 12:41 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-04-29 13:42 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]>
2026-04-30 15:20 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-01 16:57 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-01 21:13 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-04 13:16 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-05-05 07:43 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-05 13:46 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-05 14:24 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-05 15:21 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]>
2026-05-05 19:08 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-05 19:18 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]>
2026-05-05 19:45 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-05 20:05 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-05 21:01 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]>
2026-05-06 15:22 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-05 19:19 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-26 18:12 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-05-28 11:28 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-28 11:51 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-05-29 20:08 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]>
2026-05-29 20:27 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-07-02 05:06 ` Re: Changing the state of data checksums in a running cluster Palak Chaturvedi <[email protected]>
2026-07-02 08:29 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-03-15 22:56 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]>
2026-02-09 07:52 ` Re: Changing the state of data checksums in a running cluster Michael Banck <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox