diff --git a/doc/xml/release/2020s/2026/2.60.0.xml b/doc/xml/release/2020s/2026/2.60.0.xml
index 2969ebd5f..41c13ec08 100644
--- a/doc/xml/release/2020s/2026/2.60.0.xml
+++ b/doc/xml/release/2020s/2026/2.60.0.xml
@@ -68,6 +68,19 @@
Disable read retry when filters are passed to the driver.
+
+
+
+
+
+
+
+
+
+
+
+ Fix async archive-push stalling on a zero-size backup history file.
+
diff --git a/doc/xml/release/contributor.xml b/doc/xml/release/contributor.xml
index 04ec549de..ac599d353 100644
--- a/doc/xml/release/contributor.xml
+++ b/doc/xml/release/contributor.xml
@@ -95,6 +95,11 @@
AndrewJackson2020
+
+ Andrew Kroh
+ andrewkroh
+
+
Andrew L'Ecuyer
andrewlecuyer
diff --git a/src/command/archive/push/push.c b/src/command/archive/push/push.c
index 67f8a8805..dfcf66005 100644
--- a/src/command/archive/push/push.c
+++ b/src/command/archive/push/push.c
@@ -553,18 +553,38 @@ cmdArchivePushAsync(void)
// If not dropping then limit the number of WAL files pushed in a single run to archive-push-batch-size. The queue is
// only rechecked at the start of each run so this bounds how large it can grow before the next run rechecks it. Convert
- // the size to a segment count using the size of the first ready segment and always push at least one segment. A ready
- // WAL segment should never be missing or zero size, so error immediately if it is.
+ // the size to a segment count using the size of the first ready segment and always push at least one file. Files that
+ // are not segments also count against the batch, so a run may push only those files when the batch size is small, but
+ // they are acknowledged and drop out of the list for the next run.
if (!drop)
{
- const uint64_t segmentSize = storageInfoP(
- storagePg(), strNewFmt("%s/%s", strZ(jobData.walPath), strZ(strLstGet(jobData.walFileList, 0)))).size;
- CHECK_FMT(FormatError, segmentSize != 0, "size of WAL segment '%s' is 0", strZ(strLstGet(jobData.walFileList, 0)));
+ // Find the first ready WAL segment. The ready list also contains backup and timeline history files, which are
+ // smaller than a segment and may even be zero size when PostgreSQL did not sync them before the cluster was
+ // copied, so they cannot be used to determine the segment size.
+ const String *walSegment = NULL;
- const uint64_t batchMax = cfgOptionUInt64(cfgOptArchivePushBatchSize) / segmentSize;
+ for (unsigned int walFileIdx = 0; walFileIdx < strLstSize(jobData.walFileList); walFileIdx++)
+ {
+ if (walIsSegment(strLstGet(jobData.walFileList, walFileIdx)))
+ {
+ walSegment = strLstGet(jobData.walFileList, walFileIdx);
+ break;
+ }
+ }
- while (strLstSize(jobData.walFileList) > 1 && strLstSize(jobData.walFileList) > batchMax)
- strLstRemoveIdx(jobData.walFileList, strLstSize(jobData.walFileList) - 1);
+ // Apply the batch size when a segment is ready. When none is ready every file is small so there is nothing to
+ // limit. A ready WAL segment should never be missing or zero size, so error immediately if it is.
+ if (walSegment != NULL)
+ {
+ const uint64_t segmentSize = storageInfoP(
+ storagePg(), strNewFmt("%s/%s", strZ(jobData.walPath), strZ(walSegment))).size;
+ CHECK_FMT(FormatError, segmentSize != 0, "size of WAL segment '%s' is 0", strZ(walSegment));
+
+ const uint64_t batchMax = cfgOptionUInt64(cfgOptArchivePushBatchSize) / segmentSize;
+
+ while (strLstSize(jobData.walFileList) > 1 && strLstSize(jobData.walFileList) > batchMax)
+ strLstRemoveIdx(jobData.walFileList, strLstSize(jobData.walFileList) - 1);
+ }
}
LOG_INFO_FMT(
diff --git a/test/src/module/command/archive/pushTest.c b/test/src/module/command/archive/pushTest.c
index 5dacac7bf..d3b1a5d16 100644
--- a/test/src/module/command/archive/pushTest.c
+++ b/test/src/module/command/archive/pushTest.c
@@ -1135,6 +1135,51 @@ testRun(void)
TEST_ERROR(
cmdArchivePushAsync(), FormatError, "size of WAL segment '000000010000000100000016' is 0");
+ // -------------------------------------------------------------------------------------------------------------------------
+ TEST_TITLE("zero-size backup history file before the first ready segment");
+
+ HRN_STORAGE_PATH_REMOVE(storageSpoolWrite(), STORAGE_SPOOL_ARCHIVE_OUT, .recurse = true);
+ HRN_STORAGE_PATH_CREATE(storageSpoolWrite(), STORAGE_SPOOL_ARCHIVE_OUT);
+ HRN_STORAGE_PATH_REMOVE(storagePgWrite(), "pg_xlog/archive_status", .recurse = true);
+ HRN_STORAGE_PATH_CREATE(storagePgWrite(), "pg_xlog/archive_status");
+
+ HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "pg_xlog/000000010000000100000018.00000028.backup");
+ HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "pg_xlog/archive_status/000000010000000100000018.00000028.backup.ready");
+ HRN_STORAGE_PUT(storagePgWrite(), "pg_xlog/000000010000000100000019", walBufferBatch);
+ HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "pg_xlog/archive_status/000000010000000100000019.ready");
+
+ HRN_CFG_LOAD(cfgCmdArchivePush, argList, .role = cfgCmdRoleAsync);
+
+ TEST_RESULT_VOID(cmdArchivePushAsync(), "push backup history file and WAL segment");
+ TEST_RESULT_LOG(
+ "P00 INFO: push 2 WAL file(s) to archive: 000000010000000100000018.00000028.backup...000000010000000100000019\n"
+ "P01 DETAIL: pushed WAL file '000000010000000100000018.00000028.backup' to the archive\n"
+ "P01 DETAIL: pushed WAL file '000000010000000100000019' to the archive");
+
+ TEST_STORAGE_LIST(
+ storageSpool(), STORAGE_SPOOL_ARCHIVE_OUT,
+ "000000010000000100000018.00000028.backup.ok\n"
+ "000000010000000100000019.ok\n",
+ .comment = "check status files");
+
+ // -------------------------------------------------------------------------------------------------------------------------
+ TEST_TITLE("no ready segments");
+
+ HRN_STORAGE_PATH_REMOVE(storageSpoolWrite(), STORAGE_SPOOL_ARCHIVE_OUT, .recurse = true);
+ HRN_STORAGE_PATH_CREATE(storageSpoolWrite(), STORAGE_SPOOL_ARCHIVE_OUT);
+ HRN_STORAGE_PATH_REMOVE(storagePgWrite(), "pg_xlog/archive_status", .recurse = true);
+ HRN_STORAGE_PATH_CREATE(storagePgWrite(), "pg_xlog/archive_status");
+
+ HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "pg_xlog/00000002.history");
+ HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "pg_xlog/archive_status/00000002.history.ready");
+
+ TEST_RESULT_VOID(cmdArchivePushAsync(), "push history file");
+ TEST_RESULT_LOG(
+ "P00 INFO: push 1 WAL file(s) to archive: 00000002.history\n"
+ "P01 DETAIL: pushed WAL file '00000002.history' to the archive");
+
+ TEST_STORAGE_LIST(storageSpool(), STORAGE_SPOOL_ARCHIVE_OUT, "00000002.history.ok\n", .comment = "check status files");
+
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("archive-push-queue-max is checked against the full queue, not the batch");