From 477a834f801b6b7571ad0d7343cfb249c6a368a2 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Sun, 23 Aug 2026 12:15:15 +0300
Subject: [PATCH v2] Make test WAL archiving publish files atomically

An interrupted archive command can leave a partial WAL segment visible
under its final name.  A restore command running against the same
archive can then copy the segment before a later archive attempt
replaces it, making recovery tests fail intermittently.

Have PostgreSQL::Test::Cluster copy WAL files to per-node temporary
paths and rename them into place only after copying completes.  Document
that archive commands with concurrent readers should likewise publish
complete files atomically without unconditionally overwriting a
pre-existing file.

Discussion: https://postgr.es/m/8fd856ff-b37c-4731-a10b-202ef53a679d@gmail.com
---
 doc/src/sgml/backup.sgml                 | 16 ++++++++++++---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++----
 2 files changed, 34 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 168444eccc5..e34ae12fa5d 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -632,9 +632,10 @@ archive_command = 'copy "%p" "C:\\server\\archivedir\\%f"'  # Windows
 </programlisting>
     which will copy archivable WAL segments to the directory
     <filename>/mnt/server/archivedir</filename>.  (This is an example, not a
-    recommendation, and might not work on all platforms.)  After the
-    <literal>%p</literal> and <literal>%f</literal> parameters have been replaced,
-    the actual command executed might look like this:
+    recommendation, and might not work on all platforms.  In particular,
+    concurrent readers can see a partially copied file.)  After the
+    <literal>%p</literal> and <literal>%f</literal> parameters have been
+    replaced, the actual command executed might look like this:
 <programlisting>
 test ! -f /mnt/server/archivedir/00000001000000A900000065 &amp;&amp; cp pg_wal/00000001000000A900000065 /mnt/server/archivedir/00000001000000A900000065
 </programlisting>
@@ -679,6 +680,15 @@ test ! -f /mnt/server/archivedir/00000001000000A900000065 &amp;&amp; cp pg_wal/0
     not reported in <xref linkend="pg-stat-archiver-view"/>.
    </para>
 
+   <para>
+    If archive files can be read while archiving is in progress, an archive
+    command or library should make each file visible under its final name only
+    after it has been completely written.  For example, it can write a
+    temporary file in the same file system and then publish the file
+    atomically.  This must not unconditionally overwrite an existing file;
+    pre-existing files should be handled as described below.
+   </para>
+
    <para>
     Archive commands and libraries should generally be designed to refuse to overwrite
     any pre-existing archive file.  This is an important safety feature to
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 920d831be9e..87289f743f9 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1541,10 +1541,27 @@ sub enable_archiving
 	# first. Paths also need to be double-quoted to prevent failures where
 	# the path contains spaces.
 	$path =~ s{\\}{\\\\}g if ($PostgreSQL::Test::Utils::windows_os);
-	my $copy_command =
-	  $PostgreSQL::Test::Utils::windows_os
-	  ? qq{copy "%p" "$path\\\\%f"}
-	  : qq{cp "%p" "$path/%f"};
+	# Copy to a temporary file in the archive directory, then publish it under
+	# the name expected by restore_command.  In particular, don't leave a
+	# partially-copied file visible if the archive command is interrupted.
+	# A test process's PID distinguishes concurrent tests, while the port
+	# distinguishes nodes belonging to the same test.  Since each node runs
+	# archive commands serially, retries can safely reuse the temporary path.
+	my $temp_suffix = "$$." . $self->port;
+	my $copy_command;
+
+	if ($PostgreSQL::Test::Utils::windows_os)
+	{
+		my $temp_path = "$path\\\\.%f.tmp.$temp_suffix";
+		$copy_command = qq{copy /Y "%p" "$temp_path"}
+		  . qq{ && move /Y "$temp_path" "$path\\\\%f"};
+	}
+	else
+	{
+		my $temp_path = "$path/.%f.tmp.$temp_suffix";
+		$copy_command = qq{cp "%p" "$temp_path"}
+		  . qq{ && mv -f "$temp_path" "$path/%f"};
+	}
 
 	# Enable archive_mode and archive_command on node
 	$self->append_conf(
-- 
That's all, folks. May the source be with you.

