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/07/25 10:50:07 UTC
[solr] branch main updated: SOLR-16490: Create v2 equivalent for core backup API (#1740)
This is an automated email from the ASF dual-hosted git repository.
gerlowskija 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 b22016cb982 SOLR-16490: Create v2 equivalent for core backup API (#1740)
b22016cb982 is described below
commit b22016cb9823c48947b85b41747ba38e48e20f34
Author: Sanjay Dutt <sa...@gmail.com>
AuthorDate: Tue Jul 25 03:50:01 2023 -0700
SOLR-16490: Create v2 equivalent for core backup API (#1740)
No v2 equivalent existed prior to this commit. The new v2 API is
`POST /api/cores/cName/backups`.
---------
Co-authored-by: Jason Gerlowski <ge...@apache.org>
---
solr/CHANGES.txt | 3 +-
.../solr/handler/IncrementalShardBackup.java | 63 ++++++--
.../java/org/apache/solr/handler/SnapShooter.java | 64 ++++++--
.../apache/solr/handler/admin/BackupCoreOp.java | 89 +++--------
.../solr/handler/admin/CoreAdminHandler.java | 3 +-
.../solr/handler/admin/api/BackupCoreAPI.java | 177 ++++++++++++++++++++
.../org/apache/solr/handler/api/V2ApiUtils.java | 5 +
.../solr/handler/admin/api/BackupCoreAPITest.java | 178 +++++++++++++++++++++
8 files changed, 490 insertions(+), 92 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d4b3494f579..bde8089eee1 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -61,7 +61,8 @@ New Features
Improvements
---------------------
-(No changes)
+* SOLR-16490: `/admin/cores?action=backupcore` now has a v2 equivalent, available at
+ `GET /api/cores/coreName/backups` (Sanjay Dutt via Jason Gerlowski)
Optimizations
---------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/IncrementalShardBackup.java b/solr/core/src/java/org/apache/solr/handler/IncrementalShardBackup.java
index a4f9d479872..74128375ad0 100644
--- a/solr/core/src/java/org/apache/solr/handler/IncrementalShardBackup.java
+++ b/solr/core/src/java/org/apache/solr/handler/IncrementalShardBackup.java
@@ -17,6 +17,8 @@
package org.apache.solr.handler;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
@@ -29,8 +31,6 @@ import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.store.Directory;
import org.apache.solr.cloud.CloudDescriptor;
import org.apache.solr.common.SolrException;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.core.DirectoryFactory;
import org.apache.solr.core.IndexDeletionPolicyWrapper;
import org.apache.solr.core.SolrCore;
@@ -39,6 +39,7 @@ import org.apache.solr.core.backup.Checksum;
import org.apache.solr.core.backup.ShardBackupId;
import org.apache.solr.core.backup.ShardBackupMetadata;
import org.apache.solr.core.backup.repository.BackupRepository;
+import org.apache.solr.jersey.SolrJerseyResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -80,7 +81,7 @@ public class IncrementalShardBackup {
this.commitNameOption = commitNameOption;
}
- public NamedList<Object> backup() throws Exception {
+ public IncrementalShardSnapshotResponse backup() throws Exception {
final IndexCommit indexCommit = getAndSaveIndexCommit();
try {
return backup(indexCommit);
@@ -134,14 +135,14 @@ public class IncrementalShardBackup {
}
// note: remember to reserve the indexCommit first so it won't get deleted concurrently
- protected NamedList<Object> backup(final IndexCommit indexCommit) throws Exception {
+ protected IncrementalShardSnapshotResponse backup(final IndexCommit indexCommit)
+ throws Exception {
assert indexCommit != null;
URI backupLocation = incBackupFiles.getBackupLocation();
log.info(
"Creating backup snapshot at {} shardBackupMetadataFile:{}", backupLocation, shardBackupId);
- NamedList<Object> details = new SimpleOrderedMap<>();
- ;
- details.add("startTime", Instant.now().toString());
+ IncrementalShardSnapshotResponse details = new IncrementalShardSnapshotResponse();
+ details.startTime = Instant.now().toString();
Collection<String> files = indexCommit.getFileNames();
Directory dir =
@@ -153,21 +154,21 @@ public class IncrementalShardBackup {
solrCore.getSolrConfig().indexConfig.lockType);
try {
BackupStats stats = incrementalCopy(files, dir);
- details.add("indexFileCount", stats.fileCount);
- details.add("uploadedIndexFileCount", stats.uploadedFileCount);
- details.add("indexSizeMB", stats.getIndexSizeMB());
- details.add("uploadedIndexFileMB", stats.getTotalUploadedMB());
+ details.indexFileCount = stats.fileCount;
+ details.uploadedIndexFileCount = stats.uploadedFileCount;
+ details.indexSizeMB = stats.getIndexSizeMB();
+ details.uploadedIndexFileMB = stats.getTotalUploadedMB();
} finally {
solrCore.getDirectoryFactory().release(dir);
}
CloudDescriptor cd = solrCore.getCoreDescriptor().getCloudDescriptor();
if (cd != null) {
- details.add("shard", cd.getShardId());
+ details.shard = cd.getShardId();
}
- details.add("endTime", Instant.now().toString());
- details.add("shardBackupId", shardBackupId.getIdAsString());
+ details.endTime = Instant.now().toString();
+ details.shardBackupId = shardBackupId.getIdAsString();
log.info(
"Done creating backup snapshot at {} shardBackupMetadataFile:{}",
backupLocation,
@@ -241,4 +242,38 @@ public class IncrementalShardBackup {
return Precision.round(totalUploadedBytes / (1024.0 * 1024), 3);
}
}
+
+ public static class IncrementalShardSnapshotResponse extends SolrJerseyResponse {
+ @Schema(description = "The time at which backup snapshot started at.")
+ @JsonProperty("startTime")
+ public String startTime;
+
+ @Schema(description = "The count of index files in the snapshot.")
+ @JsonProperty("indexFileCount")
+ public int indexFileCount;
+
+ @Schema(description = "The count of uploaded index files.")
+ @JsonProperty("uploadedIndexFileCount")
+ public int uploadedIndexFileCount;
+
+ @Schema(description = "The size of index in MB.")
+ @JsonProperty("indexSizeMB")
+ public double indexSizeMB;
+
+ @Schema(description = "The size of uploaded index in MB.")
+ @JsonProperty("uploadedIndexFileMB")
+ public double uploadedIndexFileMB;
+
+ @Schema(description = "Shard Id.")
+ @JsonProperty("shard")
+ public String shard;
+
+ @Schema(description = "The time at which backup snapshot completed at.")
+ @JsonProperty("endTime")
+ public String endTime;
+
+ @Schema(description = "ShardId of shard to which core belongs to.")
+ @JsonProperty("shardBackupId")
+ public String shardBackupId;
+ }
}
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 5590bdf344a..184d2aeb0c2 100644
--- a/solr/core/src/java/org/apache/solr/handler/SnapShooter.java
+++ b/solr/core/src/java/org/apache/solr/handler/SnapShooter.java
@@ -16,6 +16,8 @@
*/
package org.apache.solr.handler;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
@@ -44,6 +46,8 @@ import org.apache.solr.core.backup.repository.BackupRepository;
import org.apache.solr.core.backup.repository.BackupRepository.PathType;
import org.apache.solr.core.backup.repository.LocalFileSystemRepository;
import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.SolrJerseyResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -170,7 +174,7 @@ public class SnapShooter {
}
}
- public NamedList<Object> createSnapshot() throws Exception {
+ public CoreSnapshotResponse createSnapshot() throws Exception {
final IndexCommit indexCommit = getAndSaveIndexCommit();
try {
return createSnapshot(indexCommit);
@@ -246,9 +250,9 @@ public class SnapShooter {
// TODO should use Solr's ExecutorUtil
new Thread(
() -> {
- NamedList<Object> snapShootDetails;
+ NamedList<Object> snapShootDetails = new SimpleOrderedMap<>();
try {
- snapShootDetails = createSnapshot();
+ V2ApiUtils.squashIntoNamedListWithoutHeader(snapShootDetails, createSnapshot());
} catch (Exception e) {
log.error("Exception while creating snapshot", e);
snapShootDetails = new NamedList<>();
@@ -277,7 +281,7 @@ public class SnapShooter {
* @see IndexDeletionPolicyWrapper#saveCommitPoint
* @see IndexDeletionPolicyWrapper#releaseCommitPoint
*/
- protected NamedList<Object> createSnapshot(final IndexCommit indexCommit) throws Exception {
+ protected CoreSnapshotResponse createSnapshot(final IndexCommit indexCommit) throws Exception {
assert indexCommit != null;
if (log.isInfoEnabled()) {
log.info(
@@ -287,8 +291,8 @@ public class SnapShooter {
}
boolean success = false;
try {
- NamedList<Object> details = new SimpleOrderedMap<>();
- details.add("startTime", Instant.now().toString());
+ CoreSnapshotResponse details = new CoreSnapshotResponse();
+ details.startTime = Instant.now().toString();
Collection<String> files = indexCommit.getFileNames();
Directory dir =
@@ -311,13 +315,13 @@ public class SnapShooter {
String endTime = Instant.now().toString();
// DEPRECATED: fileCount for removal, replaced with indexFileCount
- details.add("fileCount", files.size());
- details.add("indexFileCount", files.size());
- details.add("status", "success");
- details.add("snapshotCompletedAt", endTime); // DEPRECATED: for removal, replaced with endTime
- details.add("endTime", endTime);
- details.add("snapshotName", snapshotName);
- details.add("directoryName", directoryName);
+ details.fileCount = files.size();
+ details.indexFileCount = files.size();
+ details.status = "success";
+ details.snapshotCompletedAt = endTime; // DEPRECATED: for removal, replaced with endTime
+ details.endTime = endTime;
+ details.snapshotName = snapshotName;
+ details.directoryName = directoryName;
if (log.isInfoEnabled()) {
log.info(
"Done creating backup snapshot: {} into {}",
@@ -389,4 +393,38 @@ public class SnapShooter {
}
public static final String DATE_FMT = "yyyyMMddHHmmssSSS";
+
+ public static class CoreSnapshotResponse extends SolrJerseyResponse {
+ @Schema(description = "The time at which snapshot started at.")
+ @JsonProperty("startTime")
+ public String startTime;
+
+ @Schema(description = "The number of files in the snapshot.")
+ @JsonProperty("fileCount")
+ public int fileCount;
+
+ @Schema(description = "The number of index files in the snapshot.")
+ @JsonProperty("indexFileCount")
+ public int indexFileCount;
+
+ @Schema(description = "The status of the snapshot")
+ @JsonProperty("status")
+ public String status;
+
+ @Schema(description = "The time at which snapshot completed at.")
+ @JsonProperty("snapshotCompletedAt")
+ public String snapshotCompletedAt;
+
+ @Schema(description = "The time at which snapshot completed at.")
+ @JsonProperty("endTime")
+ public String endTime;
+
+ @Schema(description = "The name of the snapshot")
+ @JsonProperty("snapshotName")
+ public String snapshotName;
+
+ @Schema(description = "The name of the directory where snapshot created.")
+ @JsonProperty("directoryName")
+ public String directoryName;
+ }
}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/BackupCoreOp.java b/solr/core/src/java/org/apache/solr/handler/admin/BackupCoreOp.java
index 3b2f11bec65..2f5407416c4 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/BackupCoreOp.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/BackupCoreOp.java
@@ -17,84 +17,47 @@
package org.apache.solr.handler.admin;
-import java.net.URI;
-import java.nio.file.Paths;
-import java.util.Optional;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
-import org.apache.solr.core.SolrCore;
-import org.apache.solr.core.backup.BackupFilePaths;
+import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.core.backup.ShardBackupId;
-import org.apache.solr.core.backup.repository.BackupRepository;
-import org.apache.solr.handler.IncrementalShardBackup;
-import org.apache.solr.handler.SnapShooter;
+import org.apache.solr.handler.admin.api.BackupCoreAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.SolrJerseyResponse;
class BackupCoreOp 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);
- boolean incremental = isIncrementalBackup(params);
- final String name = parseBackupName(params);
- final ShardBackupId shardBackupId = parseShardBackupId(params);
- String prevShardBackupIdStr = params.get(CoreAdminParams.PREV_SHARD_BACKUP_ID, null);
- String repoName = params.get(CoreAdminParams.BACKUP_REPOSITORY);
+ BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody =
+ new BackupCoreAPI.BackupCoreRequestBody();
+ backupCoreRequestBody.repository = params.get(CoreAdminParams.BACKUP_REPOSITORY);
+ backupCoreRequestBody.location = params.get(CoreAdminParams.BACKUP_LOCATION);
// An optional parameter to describe the snapshot to be backed-up. If this
// parameter is not supplied, the latest index commit is backed-up.
- String commitName = params.get(CoreAdminParams.COMMIT_NAME);
-
- 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");
- }
+ backupCoreRequestBody.commitName = params.get(CoreAdminParams.COMMIT_NAME);
- URI locationUri = repository.createDirectoryURI(location);
- repository.createDirectory(locationUri);
+ String cname = params.required().get(CoreAdminParams.CORE);
+ backupCoreRequestBody.backupName = parseBackupName(params);
+ boolean incremental = isIncrementalBackup(params);
+ if (incremental) {
+ backupCoreRequestBody.shardBackupId = params.required().get(CoreAdminParams.SHARD_BACKUP_ID);
+ backupCoreRequestBody.prevShardBackupId =
+ params.get(CoreAdminParams.PREV_SHARD_BACKUP_ID, null);
+ backupCoreRequestBody.incremental = true;
+ }
+ BackupCoreAPI backupCoreAPI =
+ new BackupCoreAPI(
+ it.handler.coreContainer, it.req, it.rsp, it.handler.coreAdminAsyncTracker);
+ try {
+ SolrJerseyResponse response = backupCoreAPI.createBackup(cname, backupCoreRequestBody);
+ NamedList<Object> namedList = new SimpleOrderedMap<>();
+ V2ApiUtils.squashIntoNamedListWithoutHeader(namedList, response);
+ it.rsp.addResponse(namedList);
- if (incremental) {
- if ("file".equals(locationUri.getScheme())) {
- core.getCoreContainer().assertPathAllowed(Paths.get(locationUri));
- }
- final ShardBackupId prevShardBackupId =
- prevShardBackupIdStr != null ? ShardBackupId.from(prevShardBackupIdStr) : null;
- BackupFilePaths incBackupFiles = new BackupFilePaths(repository, locationUri);
- IncrementalShardBackup incSnapShooter =
- new IncrementalShardBackup(
- repository,
- core,
- incBackupFiles,
- prevShardBackupId,
- shardBackupId,
- Optional.ofNullable(commitName));
- NamedList<Object> rsp = incSnapShooter.backup();
- it.rsp.addResponse(rsp);
- } else {
- SnapShooter snapShooter = new SnapShooter(repository, core, locationUri, name, commitName);
- // validateCreateSnapshot will create parent dirs instead of throw; that choice is dubious.
- // But we want to throw. One reason is that this dir really should, in fact must, already
- // exist here if triggered via a collection backup on a shared file system. Otherwise,
- // perhaps the FS location isn't shared -- we want an error.
- if (!snapShooter.getBackupRepository().exists(snapShooter.getLocation())) {
- throw new SolrException(
- SolrException.ErrorCode.BAD_REQUEST,
- "Directory to contain snapshots doesn't exist: "
- + snapShooter.getLocation()
- + ". "
- + "Note that Backup/Restore of a SolrCloud collection "
- + "requires a shared file system mounted at the same path on all nodes!");
- }
- snapShooter.validateCreateSnapshot();
- it.rsp.addResponse(snapShooter.createSnapshot());
- }
} catch (Exception e) {
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR,
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 e080af316bf..846055d8d21 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
@@ -55,6 +55,7 @@ import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.CoreDescriptor;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.admin.api.AllCoresStatusAPI;
+import org.apache.solr.handler.admin.api.BackupCoreAPI;
import org.apache.solr.handler.admin.api.CoreSnapshotAPI;
import org.apache.solr.handler.admin.api.CreateCoreAPI;
import org.apache.solr.handler.admin.api.InstallCoreDataAPI;
@@ -381,7 +382,7 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
@Override
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
- return List.of(CoreSnapshotAPI.class, InstallCoreDataAPI.class);
+ return List.of(CoreSnapshotAPI.class, InstallCoreDataAPI.class, BackupCoreAPI.class);
}
static {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/BackupCoreAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/BackupCoreAPI.java
new file mode 100644
index 00000000000..833149cb07b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/BackupCoreAPI.java
@@ -0,0 +1,177 @@
+/*
+ * 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.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import java.net.URI;
+import java.nio.file.Paths;
+import java.util.Optional;
+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.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.core.backup.BackupFilePaths;
+import org.apache.solr.core.backup.ShardBackupId;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.apache.solr.handler.IncrementalShardBackup;
+import org.apache.solr.handler.SnapShooter;
+import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+@Path("/cores/{coreName}/backups")
+public class BackupCoreAPI extends CoreAdminAPIBase {
+ @Inject
+ public BackupCoreAPI(
+ 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(COLL_EDIT_PERM)
+ public SolrJerseyResponse createBackup(
+ @Parameter(description = "The name of the core.") @PathParam("coreName") String coreName,
+ @Schema(description = "The POJO for representing additional backup params.") @RequestBody
+ BackupCoreRequestBody backupCoreRequestBody)
+ throws Exception {
+ ensureRequiredParameterProvided("coreName", coreName);
+ return handlePotentiallyAsynchronousTask(
+ null,
+ coreName,
+ backupCoreRequestBody.async,
+ "backup",
+ () -> {
+ try (BackupRepository repository =
+ coreContainer.newBackupRepository(backupCoreRequestBody.repository);
+ SolrCore core = coreContainer.getCore(coreName)) {
+ String location = repository.getBackupLocation(backupCoreRequestBody.location);
+ if (location == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "'location' parameter is not specified in the request body or as a default repository property");
+ }
+ URI locationUri = repository.createDirectoryURI(location);
+ repository.createDirectory(locationUri);
+
+ if (Boolean.TRUE.equals(backupCoreRequestBody.incremental)) {
+ if ("file".equals(locationUri.getScheme())) {
+ core.getCoreContainer().assertPathAllowed(Paths.get(locationUri));
+ }
+ ensureRequiredParameterProvided("shardBackupId", backupCoreRequestBody.shardBackupId);
+ final ShardBackupId shardBackupId =
+ ShardBackupId.from(backupCoreRequestBody.shardBackupId);
+ final ShardBackupId prevShardBackupId =
+ backupCoreRequestBody.prevShardBackupId != null
+ ? ShardBackupId.from(backupCoreRequestBody.prevShardBackupId)
+ : null;
+ BackupFilePaths incBackupFiles = new BackupFilePaths(repository, locationUri);
+ IncrementalShardBackup incSnapShooter =
+ new IncrementalShardBackup(
+ repository,
+ core,
+ incBackupFiles,
+ prevShardBackupId,
+ shardBackupId,
+ Optional.ofNullable(backupCoreRequestBody.commitName));
+ return incSnapShooter.backup();
+ } else {
+ SnapShooter snapShooter =
+ new SnapShooter(
+ repository,
+ core,
+ locationUri,
+ backupCoreRequestBody.backupName,
+ backupCoreRequestBody.commitName);
+ // validateCreateSnapshot will create parent dirs instead of throw; that choice is
+ // dubious.
+ // But we want to throw. One reason is that this dir really should, in fact must,
+ // already
+ // exist here if triggered via a collection backup on a shared file system. Otherwise,
+ // perhaps the FS location isn't shared -- we want an error.
+ if (!snapShooter.getBackupRepository().exists(snapShooter.getLocation())) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Directory to contain snapshots doesn't exist: "
+ + snapShooter.getLocation()
+ + ". "
+ + "Note that Backup/Restore of a SolrCloud collection "
+ + "requires a shared file system mounted at the same path on all nodes!");
+ }
+ snapShooter.validateCreateSnapshot();
+ return snapShooter.createSnapshot();
+ }
+ } catch (Exception exp) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Failed to backup core=" + coreName + " because " + exp,
+ exp);
+ }
+ });
+ }
+
+ public static class BackupCoreRequestBody extends SolrJerseyResponse {
+
+ @Schema(description = "The name of the repository to be used for backup.")
+ @JsonProperty("repository")
+ public String repository;
+
+ @Schema(description = "The path where the backup will be created")
+ @JsonProperty("location")
+ public String location;
+
+ @JsonProperty("shardBackupId")
+ public String shardBackupId;
+
+ @JsonProperty("prevShardBackupId")
+ public String prevShardBackupId;
+
+ @Schema(
+ description = "A descriptive name for the backup. Only used by non-incremental backups.")
+ @JsonProperty("name")
+ public String backupName;
+
+ @Schema(
+ description =
+ "The name of the commit which was used while taking a snapshot using the CREATESNAPSHOT command.")
+ @JsonProperty("commitName")
+ public String commitName;
+
+ @Schema(description = "To turn on incremental backup feature")
+ @JsonProperty("incremental")
+ public Boolean incremental;
+
+ @Schema(description = "Request ID to track this action which will be processed asynchronously.")
+ @JsonProperty("async")
+ public String async;
+ }
+}
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 300082406e7..454a1bcbde6 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
@@ -85,6 +85,11 @@ public class V2ApiUtils {
squashIntoNamedList(destination, mw, false);
}
+ public static void squashIntoNamedListWithoutHeader(
+ NamedList<Object> destination, JacksonReflectMapWriter mw) {
+ squashIntoNamedList(destination, mw, true);
+ }
+
public static String getMediaTypeFromWtParam(
SolrQueryRequest solrQueryRequest, String defaultMediaType) {
final String wtParam = solrQueryRequest.getParams().get(WT);
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java
new file mode 100644
index 00000000000..4504d6d77a2
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java
@@ -0,0 +1,178 @@
+/*
+ * 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 java.io.IOException;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.backup.BackupFilePaths;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.apache.solr.handler.IncrementalShardBackup;
+import org.apache.solr.handler.SnapShooter;
+import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class BackupCoreAPITest extends SolrTestCaseJ4 {
+
+ private BackupCoreAPI backupCoreAPI;
+ private static final String backupName = "my-new-backup";
+
+ @BeforeClass
+ public static void initializeCoreAndRequestFactory() throws Exception {
+ initCore("solrconfig.xml", "schema.xml");
+ lrf = h.getRequestFactory("/api", 0, 10);
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ SolrQueryRequest solrQueryRequest = req();
+ SolrQueryResponse solrQueryResponse = new SolrQueryResponse();
+ CoreContainer coreContainer = h.getCoreContainer();
+
+ CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker =
+ new CoreAdminHandler.CoreAdminAsyncTracker();
+ backupCoreAPI =
+ new BackupCoreAPI(
+ coreContainer, solrQueryRequest, solrQueryResponse, coreAdminAsyncTracker);
+ }
+
+ @Test
+ public void testCreateNonIncrementalBackupReturnsValidResponse() throws Exception {
+ BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody = createBackupCoreRequestBody();
+ backupCoreRequestBody.incremental = false;
+ backupCoreRequestBody.backupName = backupName;
+ SnapShooter.CoreSnapshotResponse response =
+ (SnapShooter.CoreSnapshotResponse)
+ backupCoreAPI.createBackup(coreName, backupCoreRequestBody);
+
+ assertEquals(backupName, response.snapshotName);
+ assertEquals("snapshot." + backupName, response.directoryName);
+ assertEquals(1, response.fileCount);
+ assertEquals(1, response.indexFileCount);
+ }
+
+ @Test
+ public void testMissingLocationParameter() throws Exception {
+ BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody = createBackupCoreRequestBody();
+ backupCoreRequestBody.location = null;
+ backupCoreRequestBody.incremental = false;
+ backupCoreRequestBody.backupName = backupName;
+ final SolrException solrException =
+ expectThrows(
+ SolrException.class,
+ () -> {
+ backupCoreAPI.createBackup(coreName, backupCoreRequestBody);
+ });
+ assertEquals(500, solrException.code());
+ assertTrue(
+ "Exception message differed from expected: " + solrException.getMessage(),
+ solrException
+ .getMessage()
+ .contains("'location' parameter is not specified in the request body"));
+ }
+
+ @Test
+ public void testMissingCoreNameParameter() throws Exception {
+ BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody = createBackupCoreRequestBody();
+ backupCoreRequestBody.location = null;
+ backupCoreRequestBody.incremental = false;
+ backupCoreRequestBody.backupName = backupName;
+
+ final SolrException solrException =
+ expectThrows(
+ SolrException.class,
+ () -> {
+ backupCoreAPI.createBackup(null, backupCoreRequestBody);
+ });
+ assertEquals(400, solrException.code());
+ assertTrue(
+ "Exception message differed from expected: " + solrException.getMessage(),
+ solrException.getMessage().contains("Missing required parameter:"));
+ }
+
+ @Test
+ public void testNonIncrementalBackupForNonExistentCore() throws Exception {
+ BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody = createBackupCoreRequestBody();
+ backupCoreRequestBody.location = null;
+ backupCoreRequestBody.incremental = false;
+ backupCoreRequestBody.backupName = backupName;
+ final SolrException solrException =
+ expectThrows(
+ SolrException.class,
+ () -> {
+ backupCoreAPI.createBackup("non-existent-core", backupCoreRequestBody);
+ });
+ assertEquals(500, solrException.code());
+ }
+
+ @Test
+ public void testCreateIncrementalBackupReturnsValidResponse() throws Exception {
+ BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody = createBackupCoreRequestBody();
+ backupCoreRequestBody.incremental = true;
+ backupCoreRequestBody.shardBackupId = "md_shard1_0";
+ IncrementalShardBackup.IncrementalShardSnapshotResponse response =
+ (IncrementalShardBackup.IncrementalShardSnapshotResponse)
+ backupCoreAPI.createBackup(coreName, backupCoreRequestBody);
+
+ assertEquals(1, response.indexFileCount);
+ assertEquals(1, response.uploadedIndexFileCount);
+ assertEquals(backupCoreRequestBody.shardBackupId, response.shardBackupId);
+ }
+
+ @AfterClass // unique core per test
+ public static void coreDestroy() {
+ deleteCore();
+ }
+
+ private Path createBackupLocation() {
+ return createTempDir().toAbsolutePath();
+ }
+
+ private URI bootstrapBackupLocation(Path locationPath) throws IOException {
+ final String locationPathStr = locationPath.toString();
+ h.getCoreContainer().getAllowPaths().add(locationPath);
+ try (BackupRepository backupRepo = h.getCoreContainer().newBackupRepository(null)) {
+ final URI locationUri = backupRepo.createDirectoryURI(locationPathStr);
+ final BackupFilePaths backupFilePaths = new BackupFilePaths(backupRepo, locationUri);
+ backupFilePaths.createIncrementalBackupFolders();
+ return locationUri;
+ }
+ }
+
+ private BackupCoreAPI.BackupCoreRequestBody createBackupCoreRequestBody() throws Exception {
+ final Path locationPath = createBackupLocation();
+ final URI locationUri = bootstrapBackupLocation(locationPath);
+ final CoreContainer cores = h.getCoreContainer();
+ cores.getAllowPaths().add(Paths.get(locationUri));
+ final BackupCoreAPI.BackupCoreRequestBody backupCoreRequestBody =
+ new BackupCoreAPI.BackupCoreRequestBody();
+ backupCoreRequestBody.location = locationPath.toString();
+ return backupCoreRequestBody;
+ }
+}