SOLR-18344: Report in-progress backup status on /replication?command=details - #4728
SOLR-18344: Report in-progress backup status on /replication?command=details#4728idantepper wants to merge 2 commits into
Conversation
…details
The "backup" key in the details response only appeared once a backup had
finished, and until then it still held the *previous* backup's
"status": "success" -- so anything polling right after issuing
command=backup read "already done" and stopped.
The plumbing already existed and was only used once: createSnapAsync takes
a Consumer<NamedList<?>> that ReplicationHandler wires to its volatile
snapShootDetails field, which getReplicationDetails already publishes. It
was invoked a single time, at the end of the snapshot.
Keep that consumer in a progressListener field and emit through it as the
copy loop advances. ReplicationHandler is unchanged.
Two in-progress shapes are added ahead of the existing terminal ones:
- "waiting for commit" -- published synchronously before the worker
thread starts, so a stale success can no longer be read as the new
backup's result. Carries no file counts, since the index commit (and
with it the file list) has not been resolved yet.
- "running" -- adds fileCount and finishedFileCount, emitted after each
backupRepo.copyFileFrom.
The existing "success" and exception payloads are unchanged. A null
snapshotName is omitted rather than reported, matching how
CoreSnapshotResponse renders a completed snapshot via putIfNotNull.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| 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. |
There was a problem hiding this comment.
so is the idea that a success is just going to hangaround for forever, or until the next snapshot is begun? I guess that is how it works...
There was a problem hiding this comment.
Yes, exactly that. snapShootDetails on ReplicationHandler is a plain volatile field that is never cleared -- getReplicationDetails publishes it under backup whenever it is non-null. It gets overwritten by the next backup, or by a snapshot deletion, and it is only back to null after a core reload or node restart, at which point the backup key is simply absent again.
That lifetime is pre-existing and this PR does not change it. What it changes is when the first overwrite lands: it used to be at the end of the next backup, so a stale success survived the entire run of the backup that was meant to replace it. Now it is replaced the moment the next backup is requested, which is the part that fixes the stale read.
You prompted me to go look at the docs, and my checklist claim was wrong: backup-restore.adoc -- "Backup Status" does document the shape of this response, it just only showed the completed success payload. I have pushed a ref-guide commit that adds the two in-progress statuses and states this retention behaviour explicitly, so a reader knows a success may describe an earlier backup rather than one just requested.
Drive-by in the same section, shout if you would rather it went separately: it said a failure reports snapShootException, which appears nowhere in the codebase -- the key is exception.
| * <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 { |
There was a problem hiding this comment.
is this a pattern that we follow elsewhere in Solr for these types of things?
There was a problem hiding this comment.
Yes -- Solr has a family of "record what the code published, then assert on the collected sequence" helpers rather than racing the thing under test. The three closest to this one:
TrackingBackupRepository(solr/test-framework/src/java/org/apache/solr/core/TrackingBackupRepository.java) is the nearest, and it is in this same feature area: it wraps the repository and records everycopyIndexFileFrom/createOutput/createDirectoryinto a synchronized list so a test can assert on what a backup actually did.AbstractIncrementalBackupTestasserts oncopiedFiles()/outputsCreated()/directoriesCreated(), as doLocalFSCloudIncrementalBackupTestand the S3/GCS/HDFS backup tests. Same seam as here -- the per-file copy inside a running backup -- and the same shape.SoftAutoCommitTest.MockEventListenerregisters aSolrEventListenerthat offers each async commit /newSearcherevent into aLinkedBlockingQueue, and the test asserts on the ordering of what arrived. That is the precedent for asserting on an asynchronous sequence rather than polling for a single state.SnapshotBackupAPITest.TrackingSnapshotBackupAPIoverridesdoSnapShootand records what the handler passed instead of running a live backup -- the same seam this test uses. TheConsumer<NamedList<?>>overload ofReplicationHandler.doSnapShootis whatReplicationHandleritself calls, so this is not a test-only backdoor.
The alternative would have been BackupStatusChecker, which polls command=details over HTTP (TestRestoreCore, TestReplicationHandlerBackup, TestStressThreadBackup). I did not use it because it is deliberately terminal-state-only -- it returns null for anything that is not success -- and its own javadoc says it is "NOT suitable/safe ... because the replication handler API provides no reliable way to check the results of a specific backup before the results of another backup may overwrite them internally". Polling it for an intermediate status would flake: on a test-sized index the copy loop can finish between two 50ms polls.
Worth recording that the new statuses do not disturb that helper either -- non-success statuses still return null, and neither the exception check nor the startsWith("Unable to delete") check can match waiting for commit / running.
Happy to add an HTTP-level assertion on top if you would prefer one, though it could only assert "not success yet" rather than a specific in-progress status.
Unrelated: the red check is TestGracefulJettyShutdown.testSingleShardInFlightRequestsDuringShutDown failing on a jetty HTTP/2 ClosedChannelException during shutdown -- nothing to do with this change; gradle check is green.
The "Backup Status" section of backup-restore.adoc documents the shape of the "backup" key in the /replication?command=details response, but only showed the completed "success" payload. Add the two in-progress statuses, and state that the reported status is retained until the next backup starts -- so a "success" may describe an earlier backup rather than one just requested. Also fix the failure key: the response carries "exception", not "snapShootException", which appears nowhere in the codebase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| @@ -0,0 +1,9 @@ | |||
| # See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc | |||
| title: /replication?command=details now reports a backup while it is still running, with file counts, instead of showing the previous backup's status until the new one completes | |||
There was a problem hiding this comment.
"repots backup details" maybe?
epugh
left a comment
There was a problem hiding this comment.
pending tests completing, and me giving it a manual test, and maybe a tweak to chagnelyg, this looks great!
epugh
left a comment
There was a problem hiding this comment.
realizing I should just mark it request changes till the changelog gets sorted ;-)
https://issues.apache.org/jira/browse/SOLR-18344
Description
The
backupkey in the/replication?command=detailsresponse is only populated once a backup has finished. While one runs there is no sign of it, and the key still holds the previous backup's payload — including"status": "success".So a client that issues
command=backupand then pollscommand=detailsreads the stalesuccessof an earlier backup as the result of the one it just started, and concludes the new backup is already done. With no prior backup the key is simply absent, so a poller cannot distinguish "running" from "never started" either.Solution
The plumbing already existed and was only used once.
SnapShooter.createSnapAsynctakes aConsumer<NamedList<?>>thatReplicationHandlerwires to its volatilesnapShootDetailsfield, whichgetReplicationDetailsalready publishes under thebackupkey — it was just invoked a single time, at the very end of the snapshot.That consumer is now kept in a
volatile progressListenerfield and emitted through as the copy loop advances, adding two in-progress statuses ahead of the existing terminal ones:statuswaiting for commitrunningbackupRepo.copyFileFromfileCount,finishedFileCountsuccess/ exceptionThe synchronous first publish is the part that fixes the stale-
successread. It carries no file counts because the index commit — and with it the list of files to copy — has not been resolved yet.All in-progress statuses carry
startTime,directoryName, andsnapshotName; a nullsnapshotNameis omitted rather than reported, matching howCoreSnapshotResponserenders a completed snapshot viaputIfNotNull.The change is purely additive to the response, involves no API change, and
ReplicationHandleris untouched — which keeps this to a single main-source file.Tests
TestSnapshotCoreBackup#testBackupReportsProgressWhileRunningcollects every status the handler would publish and asserts on the whole sequence, rather than racing a live backup by polling — so it is deterministic. It asserts that:waiting for commit, carrying neitherfileCountnorfinishedFileCount;runningreports follow, with a non-decreasingfinishedFileCount;startTime,snapshotName,directoryName);runningreport accounts for every file in the completed backup;simpleBackupCheck.I verified the test fails without the fix — with the three progress emissions removed it fails on
Backup was never reported as running../gradlew tidy updateLicenses check -x testwas run. Two tasks fail in my local environment for reasons unrelated to this change, both of which reproduce on unmodifiedmain::rat— my checkout is a git worktree, where.gitis a file rather than a directory.rat-sources.gradletreats that as "not a git repository", so the git-index lookup returnsnulland theexclude "dev-docs"/exclude "**/AGENTS.md"/exclude "**/.*"rules in that same branch never apply; rat then scans the whole tree and flags upstream files such asdev-docs/*.adoc,.github/*andAGENTS.md.:solr-ref-guide:buildLocalAntoraSite— my repository path contains a space, which thenpxinvocation does not quote (Error: Cannot find module '/Users/idantepper/Desktop/Developing').All other checks pass, including
spotlessCheck,ecjLint,forbiddenApis,validateLogCalls,validateSourcePatternsandjavadoc.AI assistance disclosure
This change was developed with the assistance of Claude (Anthropic), following the guidance in
dev-docs/how-to-contribute.adoc. The commit carries aCo-Authored-Bytrailer. I have reviewed the diff and the test in full, confirmed the test fails without the fix, and I take responsibility for the contribution.Checklist
mainbranch../gradlew check.backup-restore.adoc§"Backup Status" now documents the two in-progress statuses, and states that the reported status is retained until the next backup starts. (An earlier revision of this description claimed the ref guide did not document this response; that was wrong.)