You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/08/08 13:14:08 UTC
[solr] branch branch_9x updated: SOLR-16490 Create a v2 API for RESTORECORE functionality (#1449)
This is an automated email from the ASF dual-hosted git repository.
gerlowskija 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 30682a44d5a SOLR-16490 Create a v2 API for RESTORECORE functionality (#1449)
30682a44d5a is described below
commit 30682a44d5a3776e744b8593e89149e1e8fa25e0
Author: Sayanti <sd...@gmail.com>
AuthorDate: Thu Aug 3 21:26:52 2023 +0530
SOLR-16490 Create a v2 API for RESTORECORE functionality (#1449)
No v2 equivalent existed prior to this commit. The new V2 API is
`POST /api/cores/cName/restore {...}`.
---------
Co-authored-by: Jason Gerlowski <ge...@apache.org>
---
solr/CHANGES.txt | 3 +
.../solr/handler/admin/CoreAdminHandler.java | 4 +-
.../apache/solr/handler/admin/RestoreCoreOp.java | 96 +++---------
.../solr/handler/admin/api/RestoreCoreAPI.java | 168 +++++++++++++++++++++
.../solr/handler/admin/RestoreCoreOpTest.java | 47 ++++++
5 files changed, 246 insertions(+), 72 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index c9b78c78b5a..c0948c5208a 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -23,6 +23,9 @@ Improvements
a HTTP 503 status. Switched to 510 so that CloudSolrClient will auto-retry it and probably succeed.
(David Smiley, Alex Deparvu)
+* SOLR-16490: The semi-internal `/admin/cores?action=restorecore` API now has a v2 equivalent, available at
+ `POST /api/cores/coreName/restore {...}` (Sayanti Dey via Jason Gerlowski)
+
Optimizations
---------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java
index 846055d8d21..94212c883cf 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java
@@ -70,6 +70,7 @@ import org.apache.solr.handler.admin.api.RequestBufferUpdatesAPI;
import org.apache.solr.handler.admin.api.RequestCoreCommandStatusAPI;
import org.apache.solr.handler.admin.api.RequestCoreRecoveryAPI;
import org.apache.solr.handler.admin.api.RequestSyncShardAPI;
+import org.apache.solr.handler.admin.api.RestoreCoreAPI;
import org.apache.solr.handler.admin.api.SingleCoreStatusAPI;
import org.apache.solr.handler.admin.api.SplitCoreAPI;
import org.apache.solr.handler.admin.api.SwapCoresAPI;
@@ -382,7 +383,8 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
@Override
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
- return List.of(CoreSnapshotAPI.class, InstallCoreDataAPI.class, BackupCoreAPI.class);
+ return List.of(
+ CoreSnapshotAPI.class, InstallCoreDataAPI.class, BackupCoreAPI.class, RestoreCoreAPI.class);
}
static {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/RestoreCoreOp.java b/solr/core/src/java/org/apache/solr/handler/admin/RestoreCoreOp.java
index b8c00c8b7a2..b7f97469b49 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/RestoreCoreOp.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/RestoreCoreOp.java
@@ -17,86 +17,40 @@
package org.apache.solr.handler.admin;
-import static org.apache.solr.common.params.CommonParams.NAME;
-
-import java.net.URI;
-import org.apache.solr.cloud.CloudDescriptor;
-import org.apache.solr.cloud.ZkController;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.core.SolrCore;
-import org.apache.solr.core.backup.ShardBackupId;
-import org.apache.solr.core.backup.repository.BackupRepository;
-import org.apache.solr.handler.RestoreCore;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.api.RestoreCoreAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
class RestoreCoreOp implements CoreAdminHandler.CoreAdminOp {
@Override
public void execute(CoreAdminHandler.CallInfo it) throws Exception {
final SolrParams params = it.req.getParams();
String cname = params.required().get(CoreAdminParams.CORE);
- String name = params.get(NAME);
- String shardBackupIdStr = params.get(CoreAdminParams.SHARD_BACKUP_ID);
- String repoName = params.get(CoreAdminParams.BACKUP_REPOSITORY);
-
- if (shardBackupIdStr == null && name == null) {
- throw new SolrException(
- SolrException.ErrorCode.BAD_REQUEST,
- "Either 'name' or 'shardBackupId' must be specified");
- }
-
- ZkController zkController = it.handler.coreContainer.getZkController();
- if (zkController == null) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Only valid for SolrCloud");
- }
-
- try (BackupRepository repository = it.handler.coreContainer.newBackupRepository(repoName);
- SolrCore core = it.handler.coreContainer.getCore(cname)) {
-
- String location = repository.getBackupLocation(params.get(CoreAdminParams.BACKUP_LOCATION));
- if (location == null) {
- throw new SolrException(
- SolrException.ErrorCode.BAD_REQUEST,
- "'location' is not specified as a query"
- + " parameter or as a default repository property");
- }
+ final var requestBody = new RestoreCoreAPI.RestoreCoreRequestBody();
+ // "async" param intentionally omitted because CoreAdminHandler has already processed
+ requestBody.name = params.get(CoreAdminParams.NAME);
+ requestBody.shardBackupId = params.get(CoreAdminParams.SHARD_BACKUP_ID);
+ requestBody.location = params.get(CoreAdminParams.BACKUP_LOCATION);
+ requestBody.backupRepository = params.get(CoreAdminParams.BACKUP_REPOSITORY);
+ requestBody.validate();
+
+ final CoreContainer coreContainer = it.handler.getCoreContainer();
+ final var api =
+ new RestoreCoreAPI(coreContainer, it.req, it.rsp, it.handler.getCoreAdminAsyncTracker());
+ final var response = api.restoreCore(cname, requestBody);
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
+ }
- URI locationUri = repository.createDirectoryURI(location);
- CloudDescriptor cd = core.getCoreDescriptor().getCloudDescriptor();
- // this core must be the only replica in its shard otherwise
- // we cannot guarantee consistency between replicas because when we add data (or restore
- // index) to this replica
- Slice slice =
- zkController
- .getClusterState()
- .getCollection(cd.getCollectionName())
- .getSlice(cd.getShardId());
- if (slice.getReplicas().size() != 1 && !core.readOnly) {
- throw new SolrException(
- SolrException.ErrorCode.SERVER_ERROR,
- "Failed to restore core="
- + core.getName()
- + ", the core must be the only replica in its shard or it must be read only");
- }
+ public static RestoreCoreAPI.RestoreCoreRequestBody createRequestFromV1Params(SolrParams params) {
+ final var requestBody = new RestoreCoreAPI.RestoreCoreRequestBody();
+ // "async" param intentionally omitted because CoreAdminHandler has already processed
+ requestBody.name = params.get(CoreAdminParams.NAME);
+ requestBody.shardBackupId = params.get(CoreAdminParams.SHARD_BACKUP_ID);
+ requestBody.location = params.get(CoreAdminParams.BACKUP_LOCATION);
+ requestBody.backupRepository = params.get(CoreAdminParams.BACKUP_REPOSITORY);
- RestoreCore restoreCore;
- if (shardBackupIdStr != null) {
- final ShardBackupId shardBackupId = ShardBackupId.from(shardBackupIdStr);
- restoreCore = RestoreCore.createWithMetaFile(repository, core, locationUri, shardBackupId);
- } else {
- restoreCore = RestoreCore.create(repository, core, locationUri, name);
- }
- boolean success = restoreCore.doRestore();
- if (!success) {
- throw new SolrException(
- SolrException.ErrorCode.SERVER_ERROR, "Failed to restore core=" + core.getName());
- }
- // other replicas to-be-created will know that they are out of date by
- // looking at their term : 0 compare to term of this core : 1
- zkController
- .getShardTerms(cd.getCollectionName(), cd.getShardId())
- .ensureHighestTermsAreNotZero();
- }
+ return requestBody;
}
}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCoreAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCoreAPI.java
new file mode 100644
index 00000000000..11243630cf7
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCoreAPI.java
@@ -0,0 +1,168 @@
+/*
+ * 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.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.security.PermissionNameProvider.Name.CORE_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import java.net.URI;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.cloud.CloudDescriptor;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.Slice;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.core.backup.ShardBackupId;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.apache.solr.handler.RestoreCore;
+import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for restoring a previously taken backup to a core
+ *
+ * <p>Only valid in SolrCloud mode. This API (POST /api/cores/coreName/restore {}) is analogous to
+ * the v1 GET /solr/admin/cores?action=RESTORECORE command.
+ */
+@Path("/cores/{coreName}/restore")
+public class RestoreCoreAPI extends CoreAdminAPIBase {
+
+ @Inject
+ public RestoreCoreAPI(
+ CoreContainer coreContainer,
+ SolrQueryRequest solrQueryRequest,
+ SolrQueryResponse solrQueryResponse,
+ CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker) {
+ super(coreContainer, coreAdminAsyncTracker, solrQueryRequest, solrQueryResponse);
+ }
+
+ @POST
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ @PermissionName(CORE_EDIT_PERM)
+ public SolrJerseyResponse restoreCore(
+ @Parameter(description = "The name of the core to be restored") @PathParam("coreName")
+ String coreName,
+ RestoreCoreRequestBody requestBody)
+ throws Exception {
+ final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+ ensureRequiredParameterProvided("coreName", coreName);
+ if (requestBody == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
+ }
+ requestBody.validate();
+ AdminAPIBase.validateZooKeeperAwareCoreContainer(coreContainer);
+ return handlePotentiallyAsynchronousTask(
+ response,
+ coreName,
+ requestBody.async,
+ "restoreCore",
+ () -> {
+ try {
+ doRestore(coreName, requestBody);
+ return response;
+ } catch (Exception e) {
+ throw new CoreAdminAPIBaseException(e);
+ }
+ });
+ }
+
+ private void doRestore(String coreName, RestoreCoreRequestBody requestBody) throws Exception {
+ try (BackupRepository repository =
+ coreContainer.newBackupRepository(requestBody.backupRepository);
+ SolrCore core = coreContainer.getCore(coreName)) {
+
+ String location = repository.getBackupLocation(requestBody.location);
+ if (location == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "'location' is not specified as a query"
+ + " parameter or as a default repository property");
+ }
+
+ URI locationUri = repository.createDirectoryURI(location);
+ CloudDescriptor cd = core.getCoreDescriptor().getCloudDescriptor();
+ // this core must be the only replica in its shard otherwise
+ // we cannot guarantee consistency between replicas because when we add data (or restore
+ // index) to this replica
+ Slice slice =
+ coreContainer
+ .getZkController()
+ .getClusterState()
+ .getCollection(cd.getCollectionName())
+ .getSlice(cd.getShardId());
+ if (slice.getReplicas().size() != 1 && !core.readOnly) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Failed to restore core="
+ + core.getName()
+ + ", the core must be the only replica in its shard or it must be read only");
+ }
+
+ RestoreCore restoreCore;
+ if (requestBody.shardBackupId != null) {
+ final ShardBackupId shardBackupId = ShardBackupId.from(requestBody.shardBackupId);
+ restoreCore = RestoreCore.createWithMetaFile(repository, core, locationUri, shardBackupId);
+ } else {
+ restoreCore = RestoreCore.create(repository, core, locationUri, requestBody.name);
+ }
+ boolean success = restoreCore.doRestore();
+ if (!success) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR, "Failed to restore core=" + core.getName());
+ }
+ // other replicas to-be-created will know that they are out of date by
+ // looking at their term : 0 compare to term of this core : 1
+ coreContainer
+ .getZkController()
+ .getShardTerms(cd.getCollectionName(), cd.getShardId())
+ .ensureHighestTermsAreNotZero();
+ }
+ }
+
+ public static class RestoreCoreRequestBody implements JacksonReflectMapWriter {
+ @JsonProperty public String name;
+
+ @JsonProperty public String shardBackupId;
+
+ @JsonProperty(CoreAdminParams.BACKUP_REPOSITORY)
+ public String backupRepository;
+
+ @JsonProperty(CoreAdminParams.BACKUP_LOCATION)
+ public String location;
+
+ @JsonProperty public String async;
+
+ public void validate() {
+ if (shardBackupId == null && name == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Either 'name' or 'shardBackupId' must be specified");
+ }
+ }
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/RestoreCoreOpTest.java b/solr/core/src/test/org/apache/solr/handler/admin/RestoreCoreOpTest.java
new file mode 100644
index 00000000000..3997ea45ac6
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/RestoreCoreOpTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Test;
+
+/** Unit tests for {@link RestoreCoreOp} */
+public class RestoreCoreOpTest extends SolrTestCaseJ4 {
+
+ @Test
+ public void testConstructsValidV2RequestFromV1Params() {
+ final var params = new ModifiableSolrParams();
+ // 'name' and 'shardBackupId' are mutually exclusive in real requests, but include them both
+ // here
+ // to validate parameter mapping.
+ params.add("name", "someName");
+ params.add("shardBackupId", "someShardBackupId");
+ params.add("repository", "someRepo");
+ params.add("location", "someLocation");
+ params.add("async", "someasyncid");
+
+ final var requestBody = RestoreCoreOp.createRequestFromV1Params(params);
+
+ assertEquals("someName", requestBody.name);
+ assertEquals("someRepo", requestBody.backupRepository);
+ assertEquals("someLocation", requestBody.location);
+ assertEquals("someShardBackupId", requestBody.shardBackupId);
+ assertNull("Expected 'async' parameter to be omitted", requestBody.async);
+ }
+}