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);
+  }
+}