This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 6fcdcd25ee6 SOLR-18344: Report in-progress backup status on
/replication?command=details (#4728)
6fcdcd25ee6 is described below
commit 6fcdcd25ee695ba55bd1d7924e54b86ce2d48cdc
Author: Idan Tepper <[email protected]>
AuthorDate: Tue Aug 18 23:30:49 2026 +0300
SOLR-18344: Report in-progress backup status on
/replication?command=details (#4728)
Co-authored-by: Claude Opus 5 <[email protected]>
(cherry picked from commit 0a0c25046eac2df6278a3832a603b7ed50977511)
---
...SOLR-18344-report-in-progress-backup-status.yml | 10 ++
.../java/org/apache/solr/handler/SnapShooter.java | 57 +++++++++++-
.../solr/handler/TestSnapshotCoreBackup.java | 102 +++++++++++++++++++++
.../deployment-guide/pages/backup-restore.adoc | 37 +++++++-
4 files changed, 201 insertions(+), 5 deletions(-)
diff --git
a/changelog/unreleased/SOLR-18344-report-in-progress-backup-status.yml
b/changelog/unreleased/SOLR-18344-report-in-progress-backup-status.yml
new file mode 100644
index 00000000000..9641510d40e
--- /dev/null
+++ b/changelog/unreleased/SOLR-18344-report-in-progress-backup-status.yml
@@ -0,0 +1,10 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: /replication?command=details now reports backup details while the
backup is
+ still running, including file counts, instead of the previous backup's status
+type: fixed # added, changed, fixed, deprecated, removed, dependency_update,
security, other
+authors:
+ - name: Idan Tepper
+ nick: idantepper
+links:
+ - name: SOLR-18344
+ url: https://issues.apache.org/jira/browse/SOLR-18344
diff --git a/solr/core/src/java/org/apache/solr/handler/SnapShooter.java
b/solr/core/src/java/org/apache/solr/handler/SnapShooter.java
index 9939f07ddf6..baee015f088 100644
--- a/solr/core/src/java/org/apache/solr/handler/SnapShooter.java
+++ b/solr/core/src/java/org/apache/solr/handler/SnapShooter.java
@@ -67,6 +67,12 @@ public class SnapShooter {
private BackupRepository backupRepo = null;
private String commitName; // can be null
+ /**
+ * Receives in-progress status while the snapshot is being created, so that
it can be reported
+ * before the snapshot completes. A no-op unless set by {@link
#createSnapAsync}.
+ */
+ private volatile Consumer<NamedList<?>> progressListener = nl -> {};
+
@Deprecated
public SnapShooter(SolrCore core, String location, String snapshotName) {
String snapDirStr = null;
@@ -245,8 +251,42 @@ public class SnapShooter {
+ solrCore.getName());
}
- public void createSnapAsync(final int numberToKeep, Consumer<NamedList<?>>
result)
- throws IOException {
+ /**
+ * The status of a snapshot that has been requested but has not finished
yet. A null {@link
+ * #snapshotName} is omitted rather than reported, matching how {@link
CoreSnapshotResponse}
+ * reports the same snapshot once it has completed.
+ */
+ private NamedList<Object> inProgressDetails(String startTime, String status)
{
+ NamedList<Object> details = new SimpleOrderedMap<>();
+ details.add("startTime", startTime);
+ details.add("status", status);
+ if (snapshotName != null) {
+ details.add("snapshotName", snapshotName);
+ }
+ details.add("directoryName", directoryName);
+ return details;
+ }
+
+ /**
+ * The status of a snapshot whose files are being copied. Only reported once
the index commit has
+ * been resolved, since until then there is no file list to count.
+ *
+ * @param fileCount the total number of files this snapshot will copy
+ * @param finishedFileCount how many of them have been copied so far
+ */
+ private NamedList<Object> runningDetails(String startTime, int fileCount,
int finishedFileCount) {
+ NamedList<Object> details = inProgressDetails(startTime, RUNNING_STATUS);
+ details.add("fileCount", fileCount);
+ details.add("finishedFileCount", finishedFileCount);
+ return details;
+ }
+
+ public void createSnapAsync(final int numberToKeep, Consumer<NamedList<?>>
result) {
+ this.progressListener = result;
+ // Report before the thread starts, otherwise the previously reported
status (possibly a
+ // "success" from an earlier snapshot) stays visible until the index
commit has been resolved.
+ // The file list isn't known until then, so this status carries no file
counts.
+ result.accept(inProgressDetails(Instant.now().toString(),
WAITING_FOR_COMMIT_STATUS));
// TODO should use Solr's ExecutorUtil
new Thread(
() -> {
@@ -295,6 +335,7 @@ public class SnapShooter {
details.startTime = Instant.now().toString();
Collection<String> files = indexCommit.getFileNames();
+ progressListener.accept(runningDetails(details.startTime, files.size(),
0));
Directory dir =
solrCore
.getDirectoryFactory()
@@ -303,10 +344,13 @@ public class SnapShooter {
DirContext.DEFAULT,
solrCore.getSolrConfig().indexConfig.lockType);
try {
+ int finishedFileCount = 0;
for (String fileName : files) {
log.debug(
"Copying fileName={} from dir={} to snapshot={}", fileName, dir,
snapshotDirPath);
backupRepo.copyFileFrom(dir, fileName, snapshotDirPath);
+ progressListener.accept(
+ runningDetails(details.startTime, files.size(),
++finishedFileCount));
}
} finally {
solrCore.getDirectoryFactory().release(dir);
@@ -394,6 +438,15 @@ public class SnapShooter {
public static final String DATE_FMT = "yyyyMMddHHmmssSSS";
+ /**
+ * Status reported after a snapshot has been requested but before its index
commit -- and with it
+ * the list of files to copy -- has been resolved.
+ */
+ public static final String WAITING_FOR_COMMIT_STATUS = "waiting for commit";
+
+ /** Status reported while a snapshot's files are being copied. */
+ public static final String RUNNING_STATUS = "running";
+
public static class CoreSnapshotResponse extends SolrJerseyResponse {
@Schema(description = "The time at which snapshot started at.")
@JsonProperty("startTime")
diff --git
a/solr/core/src/test/org/apache/solr/handler/TestSnapshotCoreBackup.java
b/solr/core/src/test/org/apache/solr/handler/TestSnapshotCoreBackup.java
index 43cda66938d..6521ee69c9d 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestSnapshotCoreBackup.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestSnapshotCoreBackup.java
@@ -20,6 +20,10 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import org.apache.lucene.index.CheckIndex;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.store.Directory;
@@ -28,9 +32,12 @@ import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.TimeSource;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.admin.CoreAdminHandler;
import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.TimeOut;
import org.junit.After;
import org.junit.Before;
@@ -370,6 +377,101 @@ public class TestSnapshotCoreBackup extends
SolrTestCaseJ4 {
admin.close();
}
+ /**
+ * Backups run asynchronously, so the status reported to
/replication?command=details must
+ * describe a snapshot that is still running -- not stay silent (or keep
describing the previously
+ * completed snapshot) until it finishes.
+ *
+ * <p>Rather than racing a live backup by polling "details", this collects
every status the
+ * handler would have published and asserts on the whole sequence, which is
deterministic.
+ */
+ public void testBackupReportsProgressWhileRunning() throws Exception {
+ for (int i = 0; i < 50; i++) {
+ assertU(adoc("id", String.valueOf(i)));
+ }
+ assertU(commit());
+
+ final Path backupDir = createTempDir();
+ h.getCoreContainer().getAllowPaths().add(backupDir);
+
+ // this is what /replication?command=backup does, minus the http plumbing
+ final List<NamedList<?>> reports = new CopyOnWriteArrayList<>();
+ ReplicationHandler.doSnapShoot(
+ 0, 0, backupDir.toString(), null, null, "progress_backup",
h.getCore(), reports::add);
+
+ final TimeOut timeOut = new TimeOut(60, TimeUnit.SECONDS,
TimeSource.NANO_TIME);
+ NamedList<?> last = null;
+ while (!timeOut.hasTimedOut()) {
+ if (!reports.isEmpty()) {
+ last = reports.get(reports.size() - 1);
+ assertNull("Backup failed: " + last, last.get("exception"));
+ if ("success".equals(last.get("status"))) {
+ break;
+ }
+ }
+ timeOut.sleep(20);
+ }
+ assertNotNull("No backup status was ever reported", last);
+ assertEquals(
+ "Backup did not succeed before the TimeOut elapsed: " + last,
+ "success",
+ last.get("status"));
+ // the backup is over, so no further reports can arrive and 'reports' is
now stable
+ final int totalFileCount = ((Number) last.get("fileCount")).intValue();
+ assertTrue(
+ "Test needs a backup of more than one file, got " + totalFileCount, 1
< totalFileCount);
+
+ // the very first status is published before the index commit is resolved,
so it names no files
+ final NamedList<?> waiting = reports.get(0);
+ assertEquals(
+ "backup should first report itself as waiting: " + waiting,
+ SnapShooter.WAITING_FOR_COMMIT_STATUS,
+ waiting.get("status"));
+ assertNull("no file list is known yet: " + waiting,
waiting.get("fileCount"));
+ assertNull("no file list is known yet: " + waiting,
waiting.get("finishedFileCount"));
+
+ final List<NamedList<?>> running = reports.subList(1, reports.size() - 1);
+ assertFalse("Backup was never reported as running", running.isEmpty());
+
+ int previousFinished = -1;
+ for (NamedList<?> report : running) {
+ assertEquals(
+ "not reported as running: " + report, SnapShooter.RUNNING_STATUS,
report.get("status"));
+
+ final int finished = ((Number)
report.get("finishedFileCount")).intValue();
+ assertTrue(
+ "finishedFileCount went backwards: " + previousFinished + " -> " +
finished,
+ previousFinished <= finished);
+ previousFinished = finished;
+ }
+
+ // every in-progress status must identify the backup it is about
+ for (NamedList<?> report : reports.subList(0, reports.size() - 1)) {
+ assertNotNull("in-progress report has no startTime: " + report,
report.get("startTime"));
+ assertEquals(
+ "in-progress report names the wrong snapshot: " + report,
+ "progress_backup",
+ report.get("snapshotName"));
+ assertEquals(
+ "in-progress report has the wrong directoryName: " + report,
+ "snapshot.progress_backup",
+ report.get("directoryName"));
+ }
+
+ // by the last report before completion, every file must be accounted for
+ final NamedList<?> lastRunning = running.get(running.size() - 1);
+ assertEquals(
+ "last running report disagrees with the completed backup: " +
lastRunning,
+ totalFileCount,
+ ((Number) lastRunning.get("fileCount")).intValue());
+ assertEquals(
+ "last running report did not finish every file: " + lastRunning,
+ totalFileCount,
+ ((Number) lastRunning.get("finishedFileCount")).intValue());
+
+ simpleBackupCheck(backupDir.resolve("snapshot.progress_backup"), 50);
+ }
+
/**
* A simple sanity check that asserts the current weird behavior of
* DirectoryReader.openIfChanged() and demos how 'softCommit' can cause the
IndexReader in use by
diff --git
a/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
b/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
index c6be96086fd..4f5b86f3b64 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
@@ -129,7 +129,7 @@ The name of the commit which was used while taking a
snapshot using the CREATESN
=== Backup Status
-The `backup` operation can be monitored to see if it has completed by sending
the `details` command to the `/replication` handler, as in this example:
+The `backup` operation can be monitored by sending the `details` command to
the `/replication` handler, as in this example:
.Status API Example
[source,text]
@@ -137,7 +137,35 @@ The `backup` operation can be monitored to see if it has
completed by sending th
http://localhost:8983/solr/gettingstarted/replication?command=details&wt=xml
----
-.Output Snippet
+The `backup` section of the response describes the most recent backup on the
core, whether it is still running or has already finished.
+While a backup is in progress, `status` is one of:
+
+`waiting for commit`::
+The backup has been requested, but its index commit -- and with it the list of
files to copy -- has not been resolved yet.
+No file counts are reported.
+
+`running`::
+The backup's files are being copied.
+`fileCount` is the total number of files to copy, and `finishedFileCount` how
many of them have been copied so far.
+
+.Output Snippet: a running backup
+[source,xml]
+----
+<lst name="backup">
+ <str name="startTime">2022-02-11T17:19:33.271461700Z</str>
+ <str name="status">running</str>
+ <str name="snapshotName">my_backup</str>
+ <str name="directoryName">snapshot.my_backup</str>
+ <int name="fileCount">10</int>
+ <int name="finishedFileCount">4</int>
+</lst>
+----
+
+`snapshotName` is only reported for a named backup.
+
+Once the backup completes, `status` becomes `success`:
+
+.Output Snippet: a completed backup
[source,xml]
----
<lst name="backup">
@@ -151,7 +179,10 @@
http://localhost:8983/solr/gettingstarted/replication?command=details&wt=xml
</lst>
----
-If it failed then a `snapShootException` will be sent in the response.
+If it failed then an `exception` will be sent in the response.
+
+The reported status is retained until the next backup is started on the core,
or until the core is reloaded.
+A `success` may therefore describe an earlier backup rather than one that was
just requested; a newly requested backup replaces it with `waiting for commit`
as soon as it is accepted.
=== Restore API