This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new e59ce3c97dd SOLR-16398: Add V2 JAX-RS APIs for GET replication restore
status and commits. (#5217)
e59ce3c97dd is described below
commit e59ce3c97ddbe030eafadf0623fdcd80c4529c31
Author: Jalaz Kumar <[email protected]>
AuthorDate: Tue Oct 27 05:14:03 2026 +0530
SOLR-16398: Add V2 JAX-RS APIs for GET replication restore status and
commits. (#5217)
Co-authored-by: Eric Pugh <[email protected]>
---
...LR-16398-replication-restore-status-commits.yml | 8 +++
.../solr/client/api/endpoint/ReplicationApis.java | 18 ++++++
.../solr/client/api/model/CommitsResponse.java | 54 ++++++++++++++++
.../api/model/ReplicationDetailsResponse.java | 12 +---
.../client/api/model/RestoreStatusResponse.java | 67 ++++++++++++++++++++
.../apache/solr/handler/ReplicationHandler.java | 72 ++++++++++++----------
.../solr/handler/admin/api/CoreReplication.java | 14 +++++
.../solr/handler/admin/api/ReplicationAPIBase.java | 14 +++++
.../org/apache/solr/handler/api/V2ApiUtils.java | 4 ++
.../handler/admin/api/CoreReplicationAPITest.java | 36 +++++++++++
.../handler/admin/api/FetchCommitsAPITest.java | 57 +++++++++++++++++
.../admin/api/FetchRestoreStatusAPITest.java | 58 +++++++++++++++++
.../pages/user-managed-index-replication.adoc | 42 +++++++++++++
13 files changed, 411 insertions(+), 45 deletions(-)
diff --git
a/changelog/unreleased/SOLR-16398-replication-restore-status-commits.yml
b/changelog/unreleased/SOLR-16398-replication-restore-status-commits.yml
new file mode 100644
index 00000000000..6b2cc979d50
--- /dev/null
+++ b/changelog/unreleased/SOLR-16398-replication-restore-status-commits.yml
@@ -0,0 +1,8 @@
+title: Add V2 JAX-RS APIs for GET replication restore status and commits.
+type: added
+authors:
+ - name: Jalaz Kumar
+ nick: jaykay12
+links:
+ - name: SOLR-16398
+ url: https://issues.apache.org/jira/browse/SOLR-16398
diff --git
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
index f95335dc407..8b0d94618df 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
@@ -29,9 +29,11 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.StreamingOutput;
import java.io.IOException;
+import org.apache.solr.client.api.model.CommitsResponse;
import org.apache.solr.client.api.model.FileListResponse;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
+import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.util.CoreApiParameters;
@Path("/cores/{coreName}/replication")
@@ -50,6 +52,22 @@ public interface ReplicationApis {
@DefaultValue("true")
Boolean follower);
+ @GET
+ @CoreApiParameters
+ @Path("/restore/status")
+ @Operation(
+ summary = "Return the status of the most recent restore operation.",
+ tags = {"replication"})
+ RestoreStatusResponse fetchRestoreStatus();
+
+ @GET
+ @CoreApiParameters
+ @Path("/commits")
+ @Operation(
+ summary = "Return the index commit points retained by the specified
core.",
+ tags = {"replication"})
+ CommitsResponse fetchCommits();
+
@GET
@CoreApiParameters
@Path("/indexversion")
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/CommitsResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/CommitsResponse.java
new file mode 100644
index 00000000000..6eea2d07bd8
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/CommitsResponse.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import org.apache.solr.client.api.util.ReflectWritable;
+
+/** Response body for the `GET /api/cores/coreName/replication/commits` API */
+public class CommitsResponse extends SolrJerseyResponse {
+
+ @JsonProperty("commits")
+ @Schema(description = "Index commit points currently retained by the
deletion policy")
+ public List<CommitDetails> commits;
+
+ @JsonProperty("status")
+ @Schema(description = "Request status, typically OK")
+ public String status;
+
+ /** Details of a single index commit point. */
+ public static class CommitDetails implements ReflectWritable {
+ @JsonProperty("indexVersion")
+ public Long indexVersion;
+
+ @JsonProperty("generation")
+ public Long generation;
+
+ @JsonProperty("filelist")
+ public List<String> filelist;
+
+ public CommitDetails() {}
+
+ public CommitDetails(long indexVersion, long generation, List<String>
filelist) {
+ this.indexVersion = indexVersion;
+ this.generation = generation;
+ this.filelist = filelist;
+ }
+ }
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java
index 9a1054c5240..a4215970f71 100644
---
a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java
+++
b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java
@@ -19,6 +19,7 @@ package org.apache.solr.client.api.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
+import org.apache.solr.client.api.model.CommitsResponse.CommitDetails;
import org.apache.solr.client.api.util.ReflectWritable;
public class ReplicationDetailsResponse extends SolrJerseyResponse {
@@ -72,17 +73,6 @@ public class ReplicationDetailsResponse extends
SolrJerseyResponse {
public Object backup;
}
- public static class CommitDetails implements ReflectWritable {
- @JsonProperty("indexVersion")
- public Long indexVersion;
-
- @JsonProperty("generation")
- public Long generation;
-
- @JsonProperty("filelist")
- public List<String> filelist;
- }
-
public static class LeaderDetails implements ReflectWritable {
@JsonProperty("confFiles")
public String confFiles;
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/RestoreStatusResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/RestoreStatusResponse.java
new file mode 100644
index 00000000000..8938c1a7ecc
--- /dev/null
+++
b/solr/api/src/java/org/apache/solr/client/api/model/RestoreStatusResponse.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.solr.client.api.util.ReflectWritable;
+
+/** Response body for the `GET /api/cores/coreName/replication/restore` API */
+public class RestoreStatusResponse extends SolrJerseyResponse {
+
+ @JsonProperty("restorestatus")
+ @Schema(description = "State of the most recent restore operation")
+ public RestoreStatus restoreStatus;
+
+ @JsonProperty("status")
+ @Schema(description = "Request status, typically OK")
+ public String status;
+
+ /** State of a restore operation. */
+ public static class RestoreStatus implements ReflectWritable {
+ @JsonProperty("snapshotName")
+ public String snapshotName;
+
+ @JsonProperty("status")
+ @Schema(description = "State of the restore operation")
+ public RestoreState status;
+
+ @JsonProperty("exception")
+ @Schema(description = "Failure message, present only when the restore
failed with an exception")
+ public String exception;
+ }
+
+ /** State of a restore operation. */
+ public enum RestoreState {
+ NOT_STARTED("No restore actions in progress"),
+ IN_PROGRESS("In Progress"),
+ SUCCESS("success"),
+ FAILED("failed");
+
+ private final String key;
+
+ RestoreState(String key) {
+ this.key = key;
+ }
+
+ @JsonValue
+ public String getKey() {
+ return key;
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
index 8287459f28f..87ac3063b6c 100644
--- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
@@ -75,13 +75,17 @@ import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.solr.api.Api;
import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.CommitsResponse;
+import org.apache.solr.client.api.model.CommitsResponse.CommitDetails;
import org.apache.solr.client.api.model.FileMetaData;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
-import
org.apache.solr.client.api.model.ReplicationDetailsResponse.CommitDetails;
import
org.apache.solr.client.api.model.ReplicationDetailsResponse.FollowerDetails;
import
org.apache.solr.client.api.model.ReplicationDetailsResponse.LeaderDetails;
import
org.apache.solr.client.api.model.ReplicationDetailsResponse.ReplicationDetails;
+import org.apache.solr.client.api.model.RestoreStatusResponse;
+import org.apache.solr.client.api.model.RestoreStatusResponse.RestoreState;
+import org.apache.solr.client.api.model.RestoreStatusResponse.RestoreStatus;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
@@ -311,7 +315,17 @@ public class ReplicationHandler extends RequestHandlerBase
} else if (command.equalsIgnoreCase(CMD_RESTORE)) {
restore(new ModifiableSolrParams(solrParams), rsp, req);
} else if (command.equalsIgnoreCase(CMD_RESTORE_STATUS)) {
- populateRestoreStatus(rsp);
+ final CoreReplication coreReplicationAPI = new CoreReplication(core,
req, rsp);
+ final RestoreStatusResponse restoreStatus =
coreReplicationAPI.fetchRestoreStatus();
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, restoreStatus);
+ if (restoreStatus.restoreStatus.exception != null) {
+ // V1 reports a failed restore as an error response
+ rsp.getValues().remove(STATUS);
+ reportErrorOnResponse(
+ rsp,
+ "Unable to read restorestatus",
+ new Exception(restoreStatus.restoreStatus.exception));
+ }
} else if (command.equalsIgnoreCase(CMD_DELETE_BACKUP)) {
deleteSnapshot(new ModifiableSolrParams(solrParams), rsp);
} else if (command.equalsIgnoreCase(CMD_FETCH_INDEX)) {
@@ -327,7 +341,8 @@ public class ReplicationHandler extends RequestHandlerBase
reportErrorOnResponse(rsp, "No follower configured", null);
}
} else if (command.equals(CMD_SHOW_COMMITS)) {
- populateCommitInfo(rsp);
+ final CoreReplication coreReplicationAPI = new CoreReplication(core,
req, rsp);
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp,
coreReplicationAPI.fetchCommits());
} else if (command.equals(CMD_DETAILS)) {
final CoreReplication coreReplicationAPI = new CoreReplication(core,
req, rsp);
// stringifyBooleans=true: V1's command=details has always rendered
isLeader/isFollower/etc.
@@ -642,42 +657,35 @@ public class ReplicationHandler extends RequestHandlerBase
}
}
- private void populateRestoreStatus(SolrQueryResponse rsp) {
- NamedList<Object> restoreStatus = new SimpleOrderedMap<>();
- if (restoreFuture == null) {
- restoreStatus.add(STATUS, "No restore actions in progress");
- rsp.add(CMD_RESTORE_STATUS, restoreStatus);
- rsp.add(STATUS, OK_STATUS);
- return;
+ public RestoreStatusResponse getRestoreStatusResponse() {
+ RestoreStatusResponse response = new RestoreStatusResponse();
+ response.restoreStatus = new RestoreStatus();
+ response.status = OK_STATUS;
+ Future<Boolean> future = restoreFuture;
+ if (future == null) {
+ response.restoreStatus.status = RestoreState.NOT_STARTED;
+ return response;
}
- restoreStatus.add("snapshotName", currentRestoreName);
- if (restoreFuture.isDone()) {
+ response.restoreStatus.snapshotName = currentRestoreName;
+ if (future.isDone()) {
try {
- boolean success = restoreFuture.get();
- if (success) {
- restoreStatus.add(STATUS, SUCCESS);
- } else {
- restoreStatus.add(STATUS, FAILED);
- }
+ response.restoreStatus.status = future.get() ? RestoreState.SUCCESS :
RestoreState.FAILED;
} catch (Exception e) {
- restoreStatus.add(STATUS, FAILED);
- restoreStatus.add(EXCEPTION, e.getMessage());
- rsp.add(CMD_RESTORE_STATUS, restoreStatus);
- reportErrorOnResponse(rsp, "Unable to read restorestatus", e);
- return;
+ response.restoreStatus.status = RestoreState.FAILED;
+ response.restoreStatus.exception = e.getMessage() != null ?
e.getMessage() : e.toString();
}
} else {
- restoreStatus.add(STATUS, "In Progress");
+ response.restoreStatus.status = RestoreState.IN_PROGRESS;
}
-
- rsp.add(CMD_RESTORE_STATUS, restoreStatus);
- rsp.add(STATUS, OK_STATUS);
+ return response;
}
- private void populateCommitInfo(SolrQueryResponse rsp) {
- rsp.add(CMD_SHOW_COMMITS, getCommits());
- rsp.add(STATUS, OK_STATUS);
+ public CommitsResponse getCommitsResponse() {
+ CommitsResponse response = new CommitsResponse();
+ response.commits = getCommitDetails();
+ response.status = OK_STATUS;
+ return response;
}
private void doSnapShoot(SolrParams params, SolrQueryResponse rsp,
SolrQueryRequest req) {
@@ -1685,10 +1693,6 @@ public class ReplicationHandler extends
RequestHandlerBase
}
}
- private static final String SUCCESS = "success";
-
- private static final String FAILED = "failed";
-
public static final String EXCEPTION = "exception";
public static final String LEADER_URL = "leaderUrl";
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
index 9985eb05a67..ea0341eb59e 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
@@ -22,9 +22,11 @@ import jakarta.inject.Inject;
import jakarta.ws.rs.core.StreamingOutput;
import java.io.IOException;
import org.apache.solr.client.api.endpoint.ReplicationApis;
+import org.apache.solr.client.api.model.CommitsResponse;
import org.apache.solr.client.api.model.FileListResponse;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
+import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrCore;
import org.apache.solr.jersey.PermissionName;
@@ -51,6 +53,18 @@ public class CoreReplication extends ReplicationAPIBase
implements ReplicationAp
return doFetchReplicationDetails(follower == null || follower);
}
+ @Override
+ @PermissionName(CORE_READ_PERM)
+ public RestoreStatusResponse fetchRestoreStatus() {
+ return doFetchRestoreStatus();
+ }
+
+ @Override
+ @PermissionName(CORE_READ_PERM)
+ public CommitsResponse fetchCommits() {
+ return doFetchCommits();
+ }
+
@Override
@PermissionName(CORE_READ_PERM)
public IndexVersionResponse fetchIndexVersion() throws IOException {
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
index 378c5699d31..a00261b388d 100644
---
a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
+++
b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
@@ -45,10 +45,12 @@ import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.RateLimiter;
import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.CommitsResponse;
import org.apache.solr.client.api.model.FileListResponse;
import org.apache.solr.client.api.model.FileMetaData;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
+import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.FastOutputStream;
import org.apache.solr.common.util.IOUtils;
@@ -100,6 +102,18 @@ public abstract class ReplicationAPIBase extends
JerseyResource {
return replicationHandler.getReplicationDetails(showFollowerDetails);
}
+ protected RestoreStatusResponse doFetchRestoreStatus() {
+ ReplicationHandler replicationHandler =
+ (ReplicationHandler)
solrCore.getRequestHandler(ReplicationHandler.PATH);
+ return replicationHandler.getRestoreStatusResponse();
+ }
+
+ protected CommitsResponse doFetchCommits() {
+ ReplicationHandler replicationHandler =
+ (ReplicationHandler)
solrCore.getRequestHandler(ReplicationHandler.PATH);
+ return replicationHandler.getCommitsResponse();
+ }
+
protected IndexVersionResponse doFetchIndexVersion() throws IOException {
ReplicationHandler replicationHandler =
(ReplicationHandler)
solrCore.getRequestHandler(ReplicationHandler.PATH);
diff --git a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
index 478c7c09be6..0a902ae6835 100644
--- a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
+++ b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
@@ -34,6 +34,7 @@ import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
+import org.apache.solr.jersey.SolrJacksonMapper;
import org.apache.solr.response.PrometheusResponseWriter;
import org.apache.solr.response.RawResponseWriter;
import org.apache.solr.response.SolrQueryResponse;
@@ -157,6 +158,9 @@ public class V2ApiUtils {
if (stringifyBooleans && value instanceof Boolean bool) {
return bool.toString();
}
+ if (value instanceof Enum<?> e) {
+ return SolrJacksonMapper.getObjectMapper().convertValue(e, Object.class);
+ }
if (value instanceof ReflectWritable) {
NamedList<Object> result = new SimpleOrderedMap<>();
squashObjectIntoNamedList(result, value, true, stringifyBooleans);
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
index 8406f8db35b..2512e458555 100644
---
a/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
@@ -30,11 +30,14 @@ import java.util.Arrays;
import java.util.List;
import org.apache.lucene.util.Constants;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.api.model.CommitsResponse;
+import org.apache.solr.client.api.model.CommitsResponse.CommitDetails;
import org.apache.solr.client.api.model.FileListResponse;
import org.apache.solr.client.api.model.FileMetaData;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
import
org.apache.solr.client.api.model.ReplicationDetailsResponse.ReplicationDetails;
+import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
@@ -100,6 +103,39 @@ public class CoreReplicationAPITest extends SolrTestCaseJ4
{
assertSame(expectedResponse, withFollowerFalse);
}
+ @Test
+ public void testGetRestoreStatus() {
+ RestoreStatusResponse expected = new RestoreStatusResponse();
+ expected.status = "OK";
+ expected.restoreStatus = new RestoreStatusResponse.RestoreStatus();
+ expected.restoreStatus.snapshotName = "snapshot.test";
+ expected.restoreStatus.status = RestoreStatusResponse.RestoreState.FAILED;
+ expected.restoreStatus.exception = "restore failed";
+
when(mockReplicationHandler.getRestoreStatusResponse()).thenReturn(expected);
+
+ RestoreStatusResponse actual = coreReplicationAPI.doFetchRestoreStatus();
+ assertEquals(expected.status, actual.status);
+ assertEquals(expected.restoreStatus.snapshotName,
actual.restoreStatus.snapshotName);
+ assertEquals(expected.restoreStatus.status, actual.restoreStatus.status);
+ assertEquals(expected.restoreStatus.exception,
actual.restoreStatus.exception);
+ }
+
+ @Test
+ public void testGetCommits() {
+ CommitDetails commit = new CommitDetails(42L, 7L, List.of("segments_7",
"_0.cfs"));
+ CommitsResponse expected = new CommitsResponse();
+ expected.status = "OK";
+ expected.commits = List.of(commit);
+ when(mockReplicationHandler.getCommitsResponse()).thenReturn(expected);
+
+ CommitsResponse actual = coreReplicationAPI.doFetchCommits();
+ assertEquals(expected.status, actual.status);
+ assertEquals(1, actual.commits.size());
+ assertEquals(expected.commits.getFirst().indexVersion,
actual.commits.getFirst().indexVersion);
+ assertEquals(expected.commits.getFirst().generation,
actual.commits.getFirst().generation);
+ assertEquals(expected.commits.getFirst().filelist,
actual.commits.getFirst().filelist);
+ }
+
@Test
public void testGetIndexVersion() throws Exception {
IndexVersionResponse expected = new IndexVersionResponse(123L, 123L,
"testGeneration");
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/FetchCommitsAPITest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/FetchCommitsAPITest.java
new file mode 100644
index 00000000000..2d9217313df
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/FetchCommitsAPITest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.request.ReplicationApi;
+import org.apache.solr.util.ExternalPaths;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/**
+ * HTTP tests for {@code GET /api/cores/coreName/replication/commits} via the
generated SolrJ
+ * request.
+ */
+public class FetchCommitsAPITest extends SolrTestCase {
+
+ private static final String CORE_NAME = "commitsApiTestCore";
+
+ @ClassRule public static final SolrJettyTestRule solrTestRule = new
SolrJettyTestRule();
+
+ @BeforeClass
+ public static void setupSolr() throws Exception {
+ System.setProperty(ALLOW_PATHS_SYSPROP,
ExternalPaths.SERVER_HOME.toAbsolutePath().toString());
+ solrTestRule.startSolr(createTempDir());
+
solrTestRule.newCollection(CORE_NAME).withConfigSet(ExternalPaths.DEFAULT_CONFIGSET).create();
+ }
+
+ @Test
+ public void testFetchCommits() throws Exception {
+ var request = new ReplicationApi.FetchCommits(CORE_NAME);
+ var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertEquals("OK", response.status);
+ assertNotNull(response.commits);
+ assertFalse(response.commits.isEmpty());
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/FetchRestoreStatusAPITest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/FetchRestoreStatusAPITest.java
new file mode 100644
index 00000000000..993afee2027
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/FetchRestoreStatusAPITest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.api.model.RestoreStatusResponse;
+import org.apache.solr.client.solrj.request.ReplicationApi;
+import org.apache.solr.util.ExternalPaths;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/**
+ * HTTP tests for {@code GET /api/cores/coreName/replication/restore/status}
via the generated SolrJ
+ * request.
+ */
+public class FetchRestoreStatusAPITest extends SolrTestCase {
+
+ private static final String CORE_NAME = "restoreStatusApiTestCore";
+
+ @ClassRule public static final SolrJettyTestRule solrTestRule = new
SolrJettyTestRule();
+
+ @BeforeClass
+ public static void setupSolr() throws Exception {
+ System.setProperty(ALLOW_PATHS_SYSPROP,
ExternalPaths.SERVER_HOME.toAbsolutePath().toString());
+ solrTestRule.startSolr(createTempDir());
+
solrTestRule.newCollection(CORE_NAME).withConfigSet(ExternalPaths.DEFAULT_CONFIGSET).create();
+ }
+
+ @Test
+ public void testFetchRestoreStatus() throws Exception {
+ var request = new ReplicationApi.FetchRestoreStatus(CORE_NAME);
+ var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
+
+ assertNotNull(response);
+ assertNull(response.error);
+ assertEquals("OK", response.status);
+ assertNotNull(response.restoreStatus);
+ assertEquals(RestoreStatusResponse.RestoreState.NOT_STARTED,
response.restoreStatus.status);
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
b/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
index b3e8484098e..781c2c54861 100644
---
a/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
+++
b/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
@@ -427,6 +427,30 @@ Disable the specified follower from polling for changes on
the leader.
[source,bash]
http://{followerHost}:{port}/solr/{coreName}/replication?command=disablepoll
+`commits`::
+Retrieve the index commit points currently retained by the core, with each
commit's index version, generation, and the file list.
++
+[tabs#commits-request]
+======
+V1 API::
++
+====
+[source,bash]
+----
+http://{leaderHost}:{port}/solr/{coreName}/replication?command=commits
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+http://{leaderHost}:{port}/api/cores/{coreName}/replication/commits
+----
+====
+======
+
`details`::
Retrieve configuration details and current status.
+
@@ -571,8 +595,26 @@ For file system repository, location defaults to core's
dataDir (data directory)
`restorestatus`::
Check the status of a running restore operation.
+
+[tabs#restorestatus-request]
+======
+V1 API::
++
+====
[source,bash]
+----
http://{leaderHost}:{port}/solr/{coreName}/replication?command=restorestatus
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+http://{leaderHost}:{port}/api/cores/{coreName}/replication/restore/status
+----
+====
+======
+
This command is used to check the status of a restore operation.
This command takes no parameters.