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 2f302e0a626 SOLR-16462: Create v2 equivalents for core-level snapshot 
CRUD functionality (#1126)
2f302e0a626 is described below

commit 2f302e0a626e296ef2c05d0d7560a6e747948d2d
Author: John Durham <[email protected]>
AuthorDate: Tue Feb 21 09:24:43 2023 -0600

    SOLR-16462: Create v2 equivalents for core-level snapshot CRUD 
functionality (#1126)
    
    LISTSNAPSHOT, CREATESNAPSHOT, and DELETESNAPSHOT now have v2 equivalent 
APIs available at `GET /api/cores/coreName/snapshots`, `POST 
/api/cores/coreName/snapshots/snapshotName`, and `DELETE 
/api/cores/coreName/snapshots/snapshotName`, respectively.
    
    While in the code, this commit also fixes a bug in the response format of 
the v1 LISTSNAPSHOT API.
    
    Co-authored-by: Jason Gerlowski <[email protected]>
---
 solr/CHANGES.txt                                   |   4 +
 .../java/org/apache/solr/core/CoreContainer.java   |  11 +
 .../solr/handler/admin/CoreAdminHandler.java       | 285 ++++++++++++---------
 .../solr/handler/admin/CoreAdminOperation.java     |  59 ++---
 .../solr/handler/admin/CreateSnapshotOp.java       |  44 +---
 .../solr/handler/admin/DeleteSnapshotOp.java       |  30 +--
 .../solr/handler/admin/api/CoreAdminAPIBase.java   | 117 +++++++++
 .../solr/handler/admin/api/CoreSnapshotAPI.java    | 278 ++++++++++++++++++++
 .../solr/core/snapshots/TestSolrCoreSnapshots.java |  15 +-
 .../solr/handler/admin/StatsReloadRaceTest.java    |   4 +-
 .../handler/admin/api/CoreSnapshotAPITest.java     | 164 ++++++++++++
 .../deployment-guide/pages/backup-restore.adoc     | 149 ++++++-----
 .../apache/solr/common/params/CoreAdminParams.java |   3 +
 13 files changed, 885 insertions(+), 278 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index e4fd530f48c..3c1768e458e 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -148,6 +148,10 @@ Improvements
 
 * SOLR-16665: The base docker image has been upgraded from Ubuntu 20 (Focal) 
to Ubuntu 22 (Jammy). (Houston Putman)
 
+* SOLR-16462: v2 equivalents of the "Core Admin" `LISTSNAPSHOT`, 
`CREATESNAPSHOT`, and `DELETESNAPSHOT` commands are now available at
+  `GET /api/cores/coreName/snapshots`, `POST 
/api/cores/coreName/snapshots/snapshotName`, and
+  `DELETE /api/cores/coreName/snapshots/snapshotName`, respectively  (John 
Durham via Jason Gerlowski)
+
 Optimizations
 ---------------------
 
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java 
b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 21da510c60d..0e61a905dd6 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -1095,6 +1095,17 @@ public class CoreContainer {
                       .to(SolrNodeKeyPair.class)
                       .in(Singleton.class);
                 }
+              })
+          .register(
+              new AbstractBinder() {
+                @Override
+                protected void configure() {
+                  bindFactory(
+                          new InjectionFactories.SingletonFactory<>(
+                              coreAdminHandler.getCoreAdminAsyncTracker()))
+                      .to(CoreAdminHandler.CoreAdminAsyncTracker.class)
+                      .in(Singleton.class);
+                }
               });
       jerseyAppHandler = new 
ApplicationHandler(containerHandlers.getJerseyEndpoints());
     }
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 767943a4589..1769e919002 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
@@ -34,10 +34,12 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
+import org.apache.solr.api.JerseyResource;
 import org.apache.solr.cloud.CloudDescriptor;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.common.SolrException;
@@ -54,6 +56,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.CoreSnapshotAPI;
 import org.apache.solr.handler.admin.api.CreateCoreAPI;
 import org.apache.solr.handler.admin.api.MergeIndexesAPI;
 import org.apache.solr.handler.admin.api.OverseerOperationAPI;
@@ -89,16 +92,8 @@ import org.slf4j.MDC;
 public class CoreAdminHandler extends RequestHandlerBase implements 
PermissionNameProvider {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   protected final CoreContainer coreContainer;
-  protected final Map<String, Map<String, TaskObject>> requestStatusMap;
+  protected final CoreAdminAsyncTracker coreAdminAsyncTracker;
 
-  protected ExecutorService parallelExecutor =
-      ExecutorUtil.newMDCAwareFixedThreadPool(
-          50, new SolrNamedThreadFactory("parallelCoreAdminExecutor"));
-
-  protected static int MAX_TRACKED_REQUESTS = 100;
-  public static String RUNNING = "running";
-  public static String COMPLETED = "completed";
-  public static String FAILED = "failed";
   public static String RESPONSE_STATUS = "STATUS";
   public static String RESPONSE_MESSAGE = "msg";
   public static String OPERATION_RESPONSE = "response";
@@ -108,11 +103,7 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
     // Unlike most request handlers, CoreContainer initialization
     // should happen in the constructor...
     this.coreContainer = null;
-    HashMap<String, Map<String, TaskObject>> map = new HashMap<>(3, 1.0f);
-    map.put(RUNNING, Collections.synchronizedMap(new LinkedHashMap<>()));
-    map.put(COMPLETED, Collections.synchronizedMap(new LinkedHashMap<>()));
-    map.put(FAILED, Collections.synchronizedMap(new LinkedHashMap<>()));
-    requestStatusMap = Collections.unmodifiableMap(map);
+    this.coreAdminAsyncTracker = new CoreAdminAsyncTracker();
   }
 
   /**
@@ -122,11 +113,7 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
    */
   public CoreAdminHandler(final CoreContainer coreContainer) {
     this.coreContainer = coreContainer;
-    HashMap<String, Map<String, TaskObject>> map = new HashMap<>(3, 1.0f);
-    map.put(RUNNING, Collections.synchronizedMap(new LinkedHashMap<>()));
-    map.put(COMPLETED, Collections.synchronizedMap(new LinkedHashMap<>()));
-    map.put(FAILED, Collections.synchronizedMap(new LinkedHashMap<>()));
-    requestStatusMap = Collections.unmodifiableMap(map);
+    this.coreAdminAsyncTracker = new CoreAdminAsyncTracker();
   }
 
   @Override
@@ -140,9 +127,9 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
   @Override
   public void initializeMetrics(SolrMetricsContext parentContext, String 
scope) {
     super.initializeMetrics(parentContext, scope);
-    parallelExecutor =
+    coreAdminAsyncTracker.parallelExecutor =
         MetricUtils.instrumentedExecutorService(
-            parallelExecutor,
+            coreAdminAsyncTracker.parallelExecutor,
             this,
             solrMetricsContext.getMetricRegistry(),
             SolrMetricManager.mkName(
@@ -188,6 +175,15 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
     return this.coreContainer;
   }
 
+  /**
+   * The instance of CoreAdminAsyncTracker owned by this handler.
+   *
+   * @return a {@link CoreAdminAsyncTracker} instance.
+   */
+  public CoreAdminAsyncTracker getCoreAdminAsyncTracker() {
+    return coreAdminAsyncTracker;
+  }
+
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
     // Make sure the cores is enabled
@@ -198,19 +194,8 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
       }
       // boolean doPersist = false;
       final String taskId = req.getParams().get(CommonAdminParams.ASYNC);
-      final TaskObject taskObject = new TaskObject(taskId);
-
-      if (taskId != null) {
-        // Put the tasks into the maps for tracking
-        if (getRequestStatusMap(RUNNING).containsKey(taskId)
-            || getRequestStatusMap(COMPLETED).containsKey(taskId)
-            || getRequestStatusMap(FAILED).containsKey(taskId)) {
-          throw new SolrException(
-              ErrorCode.BAD_REQUEST, "Duplicate request with the same 
requestid found.");
-        }
-
-        addTask(RUNNING, taskObject);
-      }
+      final CoreAdminAsyncTracker.TaskObject taskObject =
+          new CoreAdminAsyncTracker.TaskObject(taskId);
 
       // Pick the action
       final String action = req.getParams().get(ACTION, 
STATUS.toString()).toLowerCase(Locale.ROOT);
@@ -233,32 +218,13 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
       if (taskId == null) {
         callInfo.call();
       } else {
-        try {
-          MDC.put("CoreAdminHandler.asyncId", taskId);
-          MDC.put("CoreAdminHandler.action", action);
-          parallelExecutor.execute(
-              () -> {
-                boolean exceptionCaught = false;
-                try {
-                  callInfo.call();
-                  taskObject.setRspObject(callInfo.rsp);
-                  taskObject.setOperationRspObject(callInfo.rsp);
-                } catch (Exception e) {
-                  exceptionCaught = true;
-                  taskObject.setRspObjectFromException(e);
-                } finally {
-                  removeTask("running", taskObject.taskId);
-                  if (exceptionCaught) {
-                    addTask("failed", taskObject, true);
-                  } else {
-                    addTask("completed", taskObject, true);
-                  }
-                }
-              });
-        } finally {
-          MDC.remove("CoreAdminHandler.asyncId");
-          MDC.remove("CoreAdminHandler.action");
-        }
+        coreAdminAsyncTracker.submitAsyncTask(
+            taskObject,
+            action,
+            () -> {
+              callInfo.call();
+              return callInfo.rsp;
+            });
       }
     } finally {
       rsp.setHttpCaching(false);
@@ -363,72 +329,9 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
     return coreAction.isRead ? CORE_READ_PERM : CORE_EDIT_PERM;
   }
 
-  /**
-   * Helper class to manage the tasks to be tracked. This contains the taskId, 
request and the
-   * response (if available).
-   */
-  static class TaskObject {
-    String taskId;
-    String rspInfo;
-    Object operationRspInfo;
-
-    public TaskObject(String taskId) {
-      this.taskId = taskId;
-    }
-
-    public String getRspObject() {
-      return rspInfo;
-    }
-
-    public void setRspObject(SolrQueryResponse rspObject) {
-      this.rspInfo = rspObject.getToLogAsString("TaskId: " + this.taskId);
-    }
-
-    public void setRspObjectFromException(Exception e) {
-      this.rspInfo = e.getMessage();
-    }
-
-    public Object getOperationRspObject() {
-      return operationRspInfo;
-    }
-
-    public void setOperationRspObject(SolrQueryResponse rspObject) {
-      this.operationRspInfo = rspObject.getResponse();
-    }
-  }
-
-  /** Helper method to add a task to a tracking type. */
-  void addTask(String type, TaskObject o, boolean limit) {
-    synchronized (getRequestStatusMap(type)) {
-      if (limit && getRequestStatusMap(type).size() == MAX_TRACKED_REQUESTS) {
-        String key = 
getRequestStatusMap(type).entrySet().iterator().next().getKey();
-        getRequestStatusMap(type).remove(key);
-      }
-      addTask(type, o);
-    }
-  }
-
-  private void addTask(String type, TaskObject o) {
-    synchronized (getRequestStatusMap(type)) {
-      getRequestStatusMap(type).put(o.taskId, o);
-    }
-  }
-
-  /** Helper method to remove a task from a tracking map. */
-  private void removeTask(String map, String taskId) {
-    synchronized (getRequestStatusMap(map)) {
-      getRequestStatusMap(map).remove(taskId);
-    }
-  }
-
-  /** Helper method to get a request status map given the name. */
-  Map<String, TaskObject> getRequestStatusMap(String key) {
-    return requestStatusMap.get(key);
-  }
-
   /** Method to ensure shutting down of the ThreadPool Executor. */
   public void shutdown() {
-    if (parallelExecutor != null) 
ExecutorUtil.shutdownAndAwaitTermination(parallelExecutor);
+    if (coreAdminAsyncTracker.parallelExecutor != null) 
coreAdminAsyncTracker.shutdown();
   }
 
   private static final Map<String, CoreAdminOp> opMap = new HashMap<>();
@@ -477,6 +380,11 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
     return apis;
   }
 
+  @Override
+  public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+    return List.of(CoreSnapshotAPI.class);
+  }
+
   static {
     for (CoreAdminOperation op : CoreAdminOperation.values())
       opMap.put(op.action.toString().toLowerCase(Locale.ROOT), op);
@@ -493,4 +401,133 @@ public class CoreAdminHandler extends RequestHandlerBase 
implements PermissionNa
      */
     void execute(CallInfo it) throws Exception;
   }
+
+  public static class CoreAdminAsyncTracker {
+    private static final int MAX_TRACKED_REQUESTS = 100;
+    public static final String RUNNING = "running";
+    public static final String COMPLETED = "completed";
+    public static final String FAILED = "failed";
+    public final Map<String, Map<String, TaskObject>> requestStatusMap;
+
+    private ExecutorService parallelExecutor =
+        ExecutorUtil.newMDCAwareFixedThreadPool(
+            50, new 
SolrNamedThreadFactory("parallelCoreAdminAPIBaseExecutor"));
+
+    public CoreAdminAsyncTracker() {
+      HashMap<String, Map<String, TaskObject>> map = new HashMap<>(3, 1.0f);
+      map.put(RUNNING, Collections.synchronizedMap(new LinkedHashMap<>()));
+      map.put(COMPLETED, Collections.synchronizedMap(new LinkedHashMap<>()));
+      map.put(FAILED, Collections.synchronizedMap(new LinkedHashMap<>()));
+      requestStatusMap = Collections.unmodifiableMap(map);
+    }
+
+    public void shutdown() {
+      ExecutorUtil.shutdownAndAwaitTermination(parallelExecutor);
+    }
+
+    public Map<String, TaskObject> getRequestStatusMap(String key) {
+      return requestStatusMap.get(key);
+    }
+
+    public void submitAsyncTask(
+        TaskObject taskObject, String action, Callable<SolrQueryResponse> task)
+        throws SolrException {
+      ensureTaskIdNotInUse(taskObject.taskId);
+      addTask(RUNNING, taskObject);
+
+      try {
+        MDC.put("CoreAdminHandler.asyncId", taskObject.taskId);
+        MDC.put("CoreAdminHandler.action", action);
+        parallelExecutor.execute(
+            () -> {
+              boolean exceptionCaught = false;
+              try {
+                final SolrQueryResponse response = task.call();
+                taskObject.setRspObject(response);
+                taskObject.setOperationRspObject(response);
+              } catch (Exception e) {
+                exceptionCaught = true;
+                taskObject.setRspObjectFromException(e);
+              } finally {
+                finishTask(taskObject, !exceptionCaught);
+              }
+            });
+      } finally {
+        MDC.remove("CoreAdminHandler.asyncId");
+        MDC.remove("CoreAdminHandler.action");
+      }
+    }
+
+    /** Helper method to add a task to a tracking type. */
+    private void addTask(String type, TaskObject o, boolean limit) {
+      synchronized (getRequestStatusMap(type)) {
+        if (limit && getRequestStatusMap(type).size() == MAX_TRACKED_REQUESTS) 
{
+          String key = 
getRequestStatusMap(type).entrySet().iterator().next().getKey();
+          getRequestStatusMap(type).remove(key);
+        }
+        addTask(type, o);
+      }
+    }
+
+    private void addTask(String type, TaskObject o) {
+      synchronized (getRequestStatusMap(type)) {
+        getRequestStatusMap(type).put(o.taskId, o);
+      }
+    }
+
+    /** Helper method to remove a task from a tracking map. */
+    private void removeTask(String map, String taskId) {
+      synchronized (getRequestStatusMap(map)) {
+        getRequestStatusMap(map).remove(taskId);
+      }
+    }
+
+    private void ensureTaskIdNotInUse(String taskId) throws SolrException {
+      if (getRequestStatusMap(RUNNING).containsKey(taskId)
+          || getRequestStatusMap(COMPLETED).containsKey(taskId)
+          || getRequestStatusMap(FAILED).containsKey(taskId)) {
+        throw new SolrException(
+            ErrorCode.BAD_REQUEST, "Duplicate request with the same requestid 
found.");
+      }
+    }
+
+    private void finishTask(TaskObject taskObject, boolean successful) {
+      removeTask(RUNNING, taskObject.taskId);
+      addTask(successful ? COMPLETED : FAILED, taskObject, true);
+    }
+
+    /**
+     * Helper class to manage the tasks to be tracked. This contains the 
taskId, request and the
+     * response (if available).
+     */
+    public static class TaskObject {
+      public String taskId;
+      public String rspInfo;
+      public Object operationRspInfo;
+
+      public TaskObject(String taskId) {
+        this.taskId = taskId;
+      }
+
+      public String getRspObject() {
+        return rspInfo;
+      }
+
+      public void setRspObject(SolrQueryResponse rspObject) {
+        this.rspInfo = rspObject.getToLogAsString("TaskId: " + this.taskId);
+      }
+
+      public void setRspObjectFromException(Exception e) {
+        this.rspInfo = e.getMessage();
+      }
+
+      public Object getOperationRspObject() {
+        return operationRspInfo;
+      }
+
+      public void setOperationRspObject(SolrQueryResponse rspObject) {
+        this.operationRspInfo = rspObject.getResponse();
+      }
+    }
+  }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java 
b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
index 096476e2efa..6d16bdd5fbd 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminOperation.java
@@ -42,13 +42,13 @@ import static 
org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.UNLO
 import static org.apache.solr.common.params.CoreAdminParams.REPLICA;
 import static org.apache.solr.common.params.CoreAdminParams.REPLICA_TYPE;
 import static org.apache.solr.common.params.CoreAdminParams.SHARD;
-import static org.apache.solr.handler.admin.CoreAdminHandler.COMPLETED;
 import static org.apache.solr.handler.admin.CoreAdminHandler.CallInfo;
-import static org.apache.solr.handler.admin.CoreAdminHandler.FAILED;
+import static 
org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminAsyncTracker.COMPLETED;
+import static 
org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminAsyncTracker.FAILED;
+import static 
org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminAsyncTracker.RUNNING;
 import static 
org.apache.solr.handler.admin.CoreAdminHandler.OPERATION_RESPONSE;
 import static org.apache.solr.handler.admin.CoreAdminHandler.RESPONSE_MESSAGE;
 import static org.apache.solr.handler.admin.CoreAdminHandler.RESPONSE_STATUS;
-import static org.apache.solr.handler.admin.CoreAdminHandler.RUNNING;
 import static org.apache.solr.handler.admin.CoreAdminHandler.buildCoreParams;
 import static org.apache.solr.handler.admin.CoreAdminHandler.normalizePath;
 
@@ -57,7 +57,6 @@ import java.lang.invoke.MethodHandles;
 import java.nio.file.Path;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.common.SolrException;
@@ -70,10 +69,9 @@ import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.CoreDescriptor;
 import org.apache.solr.core.SolrCore;
-import org.apache.solr.core.snapshots.SolrSnapshotManager;
-import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager;
-import 
org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.SnapshotMetaData;
 import org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminOp;
+import org.apache.solr.handler.admin.api.CoreSnapshotAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.search.SolrIndexSearcher;
 import org.apache.solr.update.UpdateLog;
 import org.apache.solr.util.NumberUtils;
@@ -219,21 +217,26 @@ public enum CoreAdminOperation implements CoreAdminOp {
         String requestId = params.required().get(CoreAdminParams.REQUESTID);
         log().info("Checking request status for : " + requestId);
 
-        if (it.handler.getRequestStatusMap(RUNNING).containsKey(requestId)) {
+        final CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker =
+            it.handler.getCoreAdminAsyncTracker();
+        if 
(coreAdminAsyncTracker.getRequestStatusMap(RUNNING).containsKey(requestId)) {
           it.rsp.add(RESPONSE_STATUS, RUNNING);
-        } else if 
(it.handler.getRequestStatusMap(COMPLETED).containsKey(requestId)) {
+        } else if 
(coreAdminAsyncTracker.getRequestStatusMap(COMPLETED).containsKey(requestId)) {
           it.rsp.add(RESPONSE_STATUS, COMPLETED);
           it.rsp.add(
               RESPONSE_MESSAGE,
-              
it.handler.getRequestStatusMap(COMPLETED).get(requestId).getRspObject());
+              
coreAdminAsyncTracker.getRequestStatusMap(COMPLETED).get(requestId).getRspObject());
           it.rsp.add(
               OPERATION_RESPONSE,
-              
it.handler.getRequestStatusMap(COMPLETED).get(requestId).getOperationRspObject());
-        } else if 
(it.handler.getRequestStatusMap(FAILED).containsKey(requestId)) {
+              coreAdminAsyncTracker
+                  .getRequestStatusMap(COMPLETED)
+                  .get(requestId)
+                  .getOperationRspObject());
+        } else if 
(coreAdminAsyncTracker.getRequestStatusMap(FAILED).containsKey(requestId)) {
           it.rsp.add(RESPONSE_STATUS, FAILED);
           it.rsp.add(
               RESPONSE_MESSAGE,
-              
it.handler.getRequestStatusMap(FAILED).get(requestId).getRspObject());
+              
coreAdminAsyncTracker.getRequestStatusMap(FAILED).get(requestId).getRspObject());
         } else {
           it.rsp.add(RESPONSE_STATUS, "notfound");
           it.rsp.add(RESPONSE_MESSAGE, "No task found in running, completed or 
failed tasks");
@@ -277,31 +280,17 @@ public enum CoreAdminOperation implements CoreAdminOp {
       LISTSNAPSHOTS,
       it -> {
         final SolrParams params = it.req.getParams();
-        String cname = params.required().get(CoreAdminParams.CORE);
+        final String coreName = params.required().get(CoreAdminParams.CORE);
 
-        CoreContainer cc = it.handler.getCoreContainer();
+        final CoreContainer coreContainer = it.handler.getCoreContainer();
+        final CoreSnapshotAPI coreSnapshotAPI =
+            new CoreSnapshotAPI(
+                it.req, it.rsp, coreContainer, 
it.handler.getCoreAdminAsyncTracker());
 
-        try (SolrCore core = cc.getCore(cname)) {
-          if (core == null) {
-            throw new SolrException(ErrorCode.BAD_REQUEST, "Unable to locate 
core " + cname);
-          }
+        final CoreSnapshotAPI.ListSnapshotsResponse response =
+            coreSnapshotAPI.listSnapshots(coreName);
 
-          SolrSnapshotMetaDataManager mgr = core.getSnapshotMetaDataManager();
-          @SuppressWarnings({"rawtypes"})
-          NamedList result = new NamedList();
-          for (String name : mgr.listSnapshots()) {
-            Optional<SnapshotMetaData> metadata = 
mgr.getSnapshotMetaData(name);
-            if (metadata.isPresent()) {
-              NamedList<String> props = new NamedList<>();
-              props.add(
-                  SolrSnapshotManager.GENERATION_NUM,
-                  String.valueOf(metadata.get().getGenerationNumber()));
-              props.add(SolrSnapshotManager.INDEX_DIR_PATH, 
metadata.get().getIndexDirPath());
-              result.add(name, props);
-            }
-          }
-          it.rsp.add(SolrSnapshotManager.SNAPSHOTS_INFO, result);
-        }
+        V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
       });
 
   final CoreAdminParams.CoreAdminAction action;
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/CreateSnapshotOp.java 
b/solr/core/src/java/org/apache/solr/handler/admin/CreateSnapshotOp.java
index d620e81df4b..6b27293c47f 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CreateSnapshotOp.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CreateSnapshotOp.java
@@ -17,50 +17,26 @@
 
 package org.apache.solr.handler.admin;
 
-import org.apache.lucene.index.IndexCommit;
-import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.IndexDeletionPolicyWrapper;
-import org.apache.solr.core.SolrCore;
-import org.apache.solr.core.snapshots.SolrSnapshotManager;
-import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager;
+import org.apache.solr.handler.admin.api.CoreSnapshotAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
 
 class CreateSnapshotOp implements CoreAdminHandler.CoreAdminOp {
   @Override
   public void execute(CoreAdminHandler.CallInfo it) throws Exception {
     final SolrParams params = it.req.getParams();
-    String commitName = params.required().get(CoreAdminParams.COMMIT_NAME);
-    String cname = params.required().get(CoreAdminParams.CORE);
+    final String coreName = params.required().get(CoreAdminParams.CORE);
+    final String commitName = 
params.required().get(CoreAdminParams.COMMIT_NAME);
 
-    CoreContainer cc = it.handler.getCoreContainer();
+    final CoreContainer coreContainer = it.handler.getCoreContainer();
+    final CoreSnapshotAPI coreSnapshotAPI =
+        new CoreSnapshotAPI(it.req, it.rsp, coreContainer, 
it.handler.getCoreAdminAsyncTracker());
 
-    try (SolrCore core = cc.getCore(cname)) {
-      if (core == null) {
-        throw new SolrException(
-            SolrException.ErrorCode.BAD_REQUEST, "Unable to locate core " + 
cname);
-      }
+    final CoreSnapshotAPI.CreateSnapshotResponse response =
+        coreSnapshotAPI.createSnapshot(coreName, commitName, null);
 
-      final String indexDirPath = core.getIndexDir();
-      final IndexDeletionPolicyWrapper delPol = core.getDeletionPolicy();
-      final IndexCommit ic = delPol.getAndSaveLatestCommit();
-      try {
-        if (null == ic) {
-          throw new SolrException(
-              SolrException.ErrorCode.BAD_REQUEST, "No index commits to 
snapshot in core " + cname);
-        }
-        final SolrSnapshotMetaDataManager mgr = 
core.getSnapshotMetaDataManager();
-        mgr.snapshot(commitName, indexDirPath, ic.getGeneration());
-
-        it.rsp.add(CoreAdminParams.CORE, core.getName());
-        it.rsp.add(CoreAdminParams.COMMIT_NAME, commitName);
-        it.rsp.add(SolrSnapshotManager.INDEX_DIR_PATH, indexDirPath);
-        it.rsp.add(SolrSnapshotManager.GENERATION_NUM, ic.getGeneration());
-        it.rsp.add(SolrSnapshotManager.FILE_LIST, ic.getFileNames());
-      } finally {
-        delPol.releaseCommitPoint(ic);
-      }
-    }
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/DeleteSnapshotOp.java 
b/solr/core/src/java/org/apache/solr/handler/admin/DeleteSnapshotOp.java
index 95b04ae6f96..a13bd6d5163 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/DeleteSnapshotOp.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/DeleteSnapshotOp.java
@@ -17,35 +17,27 @@
 
 package org.apache.solr.handler.admin;
 
-import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.admin.api.CoreSnapshotAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
 
 class DeleteSnapshotOp implements CoreAdminHandler.CoreAdminOp {
 
   @Override
   public void execute(CoreAdminHandler.CallInfo it) throws Exception {
     final SolrParams params = it.req.getParams();
-    String commitName = params.required().get(CoreAdminParams.COMMIT_NAME);
-    String cname = params.required().get(CoreAdminParams.CORE);
+    final String commitName = 
params.required().get(CoreAdminParams.COMMIT_NAME);
+    final String coreName = params.required().get(CoreAdminParams.CORE);
 
-    CoreContainer cc = it.handler.getCoreContainer();
-    SolrCore core = cc.getCore(cname);
-    if (core == null) {
-      throw new SolrException(
-          SolrException.ErrorCode.BAD_REQUEST, "Unable to locate core " + 
cname);
-    }
+    final CoreContainer coreContainer = it.handler.getCoreContainer();
+    final CoreSnapshotAPI coreSnapshotAPI =
+        new CoreSnapshotAPI(it.req, it.rsp, coreContainer, 
it.handler.getCoreAdminAsyncTracker());
 
-    try {
-      core.deleteNamedSnapshot(commitName);
-      // Ideally we shouldn't need this. This is added since the RPC logic in
-      // OverseerCollectionMessageHandler can not provide the coreName as part 
of the result.
-      it.rsp.add(CoreAdminParams.CORE, core.getName());
-      it.rsp.add(CoreAdminParams.COMMIT_NAME, commitName);
-    } finally {
-      core.close();
-    }
+    final CoreSnapshotAPI.DeleteSnapshotResponse response =
+        coreSnapshotAPI.deleteSnapshot(coreName, commitName, null);
+
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response);
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CoreAdminAPIBase.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreAdminAPIBase.java
new file mode 100644
index 00000000000..a5d9431cc7f
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreAdminAPIBase.java
@@ -0,0 +1,117 @@
+/*
+ * 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.util.function.Supplier;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.logging.MDCLoggingContext;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.tracing.TraceUtils;
+
+/**
+ * A common parent for admin Core Jersey-based APIs.
+ *
+ * <p>This base class is used when creating Core APIs to allow extra 
bookkeeping tasks such as async
+ * requests handling.
+ */
+public abstract class CoreAdminAPIBase extends JerseyResource {
+
+  protected final CoreContainer coreContainer;
+  protected final CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker;
+  protected final SolrQueryRequest req;
+  protected final SolrQueryResponse rsp;
+
+  public CoreAdminAPIBase(
+      CoreContainer coreContainer,
+      CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker,
+      SolrQueryRequest req,
+      SolrQueryResponse rsp) {
+    this.coreContainer = coreContainer;
+    this.coreAdminAsyncTracker = coreAdminAsyncTracker;
+    this.req = req;
+    this.rsp = rsp;
+  }
+
+  /**
+   * Wraps the subclasses logic with extra bookkeeping logic.
+   *
+   * <p>This method currently exists to enable async handling behavior for V2 
Core APIs.
+   *
+   * <p>Since the logic for a given API lives inside the Supplier functional 
interface, checked
+   * exceptions can't be thrown directly to the calling method. To throw a 
checked exception out of
+   * the Supplier, wrap the exception using {@link 
CoreAdminAPIBase.CoreAdminAPIBaseException} and
+   * throw it instead. This handle method will retrieve the checked exception 
from {@link
+   * CoreAdminAPIBaseException} and throw it to the original calling method.
+   *
+   * @param solrJerseyResponse the response that the calling methods expects 
to return.
+   * @param coreName the name of the core that work is being done against.
+   * @param taskId an id provided for registering async work (if null, the 
task is executed
+   *     synchronously)
+   * @param actionName a name for the action being done.
+   * @param supplier the work that the calling method wants done.
+   * @return the supplied T solrJerseyResponse
+   */
+  public <T extends SolrJerseyResponse> T handlePotentiallyAsynchronousTask(
+      T solrJerseyResponse, String coreName, String taskId, String actionName, 
Supplier<T> supplier)
+      throws Exception {
+    try {
+      if (coreContainer == null) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Core container instance 
missing");
+      }
+      final CoreAdminHandler.CoreAdminAsyncTracker.TaskObject taskObject =
+          new CoreAdminHandler.CoreAdminAsyncTracker.TaskObject(taskId);
+
+      MDCLoggingContext.setCoreName(coreName);
+      TraceUtils.setDbInstance(req, coreName);
+      if (taskId == null) {
+        return supplier.get();
+      } else {
+        coreAdminAsyncTracker.submitAsyncTask(
+            taskObject,
+            actionName,
+            () -> {
+              T response = supplier.get();
+              V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
+              return rsp;
+            });
+      }
+    } catch (CoreAdminAPIBaseException e) {
+      throw e.trueException;
+    } finally {
+      rsp.setHttpCaching(false);
+    }
+
+    return solrJerseyResponse;
+  }
+  /**
+   * Helper RuntimeException to allow passing checked exceptions to the caller 
of the handle method.
+   */
+  protected static class CoreAdminAPIBaseException extends RuntimeException {
+    Exception trueException;
+
+    public CoreAdminAPIBaseException(Exception trueException) {
+      this.trueException = trueException;
+    }
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CoreSnapshotAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreSnapshotAPI.java
new file mode 100644
index 00000000000..b5e028b8cf7
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreSnapshotAPI.java
@@ -0,0 +1,278 @@
+/*
+ * 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 static 
org.apache.solr.security.PermissionNameProvider.Name.CORE_READ_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import org.apache.lucene.index.IndexCommit;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.IndexDeletionPolicyWrapper;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.core.snapshots.SolrSnapshotManager;
+import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager;
+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 Creating, Listing, and Deleting Core Snapshots. */
+@Path("/cores/{coreName}/snapshots")
+public class CoreSnapshotAPI extends CoreAdminAPIBase {
+
+  @Inject
+  public CoreSnapshotAPI(
+      SolrQueryRequest request,
+      SolrQueryResponse response,
+      CoreContainer coreContainer,
+      CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker) {
+    super(coreContainer, coreAdminAsyncTracker, request, response);
+  }
+
+  /** This API is analogous to V1 (POST 
/solr/admin/cores?action=CREATESNAPSHOT) */
+  @POST
+  @Path("/{snapshotName}")
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(CORE_EDIT_PERM)
+  public CreateSnapshotResponse createSnapshot(
+      @Parameter(description = "The name of the core to snapshot.", required = 
true)
+          @PathParam("coreName")
+          String coreName,
+      @Parameter(description = "The name to associate with the core 
snapshot.", required = true)
+          @PathParam("snapshotName")
+          String snapshotName,
+      @Parameter(description = "The id to associate with the async task.") 
@QueryParam("async")
+          String taskId)
+      throws Exception {
+    final CreateSnapshotResponse response = 
instantiateJerseyResponse(CreateSnapshotResponse.class);
+
+    return handlePotentiallyAsynchronousTask(
+        response,
+        coreName,
+        taskId,
+        "createSnapshot",
+        () -> {
+          try (SolrCore core = coreContainer.getCore(coreName)) {
+            if (core == null) {
+              throw new SolrException(
+                  SolrException.ErrorCode.BAD_REQUEST, "Unable to locate core 
" + coreName);
+            }
+
+            final String indexDirPath = core.getIndexDir();
+            final IndexDeletionPolicyWrapper delPol = core.getDeletionPolicy();
+            final IndexCommit ic = delPol.getAndSaveLatestCommit();
+            try {
+              if (null == ic) {
+                throw new SolrException(
+                    SolrException.ErrorCode.BAD_REQUEST,
+                    "No index commits to snapshot in core " + coreName);
+              }
+              final SolrSnapshotMetaDataManager mgr = 
core.getSnapshotMetaDataManager();
+              mgr.snapshot(snapshotName, indexDirPath, ic.getGeneration());
+
+              response.core = core.getName();
+              response.commitName = snapshotName;
+              response.indexDirPath = indexDirPath;
+              response.generation = ic.getGeneration();
+              response.files = ic.getFileNames();
+            } catch (IOException e) {
+              throw new CoreAdminAPIBaseException(e);
+            } finally {
+              delPol.releaseCommitPoint(ic);
+            }
+          }
+
+          return response;
+        });
+  }
+
+  /** The Response for {@link CoreSnapshotAPI}'s {@link 
#createSnapshot(String, String, String)} */
+  public static class CreateSnapshotResponse extends SolrJerseyResponse {
+    @Schema(description = "The name of the core.")
+    @JsonProperty(CoreAdminParams.CORE)
+    public String core;
+
+    @Schema(description = "The name of the created snapshot.")
+    @JsonProperty(CoreAdminParams.SNAPSHOT_NAME)
+    public String commitName;
+
+    @Schema(description = "The path to the directory containing the index 
files.")
+    @JsonProperty(SolrSnapshotManager.INDEX_DIR_PATH)
+    public String indexDirPath;
+
+    @Schema(description = "The generation value for the created snapshot.")
+    @JsonProperty(SolrSnapshotManager.GENERATION_NUM)
+    public Long generation;
+
+    @Schema(description = "The list of index filenames contained within the 
created snapshot.")
+    @JsonProperty(SolrSnapshotManager.FILE_LIST)
+    public Collection<String> files;
+  }
+
+  /** This API is analogous to V1 (GET /solr/admin/cores?action=LISTSNAPSHOTS) 
*/
+  @GET
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(CORE_READ_PERM)
+  public ListSnapshotsResponse listSnapshots(
+      @Parameter(
+              description = "The name of the core for which to retrieve 
snapshots.",
+              required = true)
+          @PathParam("coreName")
+          String coreName)
+      throws Exception {
+    final ListSnapshotsResponse response = 
instantiateJerseyResponse(ListSnapshotsResponse.class);
+
+    return handlePotentiallyAsynchronousTask(
+        response,
+        coreName,
+        null, // 'list' operations are never asynchronous
+        "listSnapshots",
+        () -> {
+          try (SolrCore core = coreContainer.getCore(coreName)) {
+            if (core == null) {
+              throw new SolrException(
+                  SolrException.ErrorCode.BAD_REQUEST, "Unable to locate core 
" + coreName);
+            }
+
+            SolrSnapshotMetaDataManager mgr = 
core.getSnapshotMetaDataManager();
+
+            final Map<String, SnapshotInformation> result = new HashMap<>();
+            for (String name : mgr.listSnapshots()) {
+              Optional<SolrSnapshotMetaDataManager.SnapshotMetaData> metadata =
+                  mgr.getSnapshotMetaData(name);
+              if (metadata.isPresent()) {
+                final SnapshotInformation snapshotInformation =
+                    new SnapshotInformation(
+                        metadata.get().getGenerationNumber(), 
metadata.get().getIndexDirPath());
+                result.put(name, snapshotInformation);
+              }
+            }
+
+            response.snapshots = result;
+          }
+
+          return response;
+        });
+  }
+
+  /** The Response for {@link CoreSnapshotAPI}'s {@link 
#listSnapshots(String)} */
+  public static class ListSnapshotsResponse extends SolrJerseyResponse {
+    @Schema(description = "The collection of snapshots found for the requested 
core.")
+    @JsonProperty(SolrSnapshotManager.SNAPSHOTS_INFO)
+    public Map<String, SnapshotInformation> snapshots;
+  }
+
+  /**
+   * Contained in {@link ListSnapshotsResponse}, this holds information for a 
given core's Snapshot
+   */
+  public static class SnapshotInformation implements JacksonReflectMapWriter {
+    @Schema(description = "The generation value for the snapshot.")
+    @JsonProperty(SolrSnapshotManager.GENERATION_NUM)
+    public final long generationNumber;
+
+    @Schema(description = "The path to the directory containing the index 
files.")
+    @JsonProperty(SolrSnapshotManager.INDEX_DIR_PATH)
+    public final String indexDirPath;
+
+    public SnapshotInformation(long generationNumber, String indexDirPath) {
+      this.generationNumber = generationNumber;
+      this.indexDirPath = indexDirPath;
+    }
+  }
+
+  /** This API is analogous to V1 (DELETE 
/solr/admin/cores?action=DELETESNAPSHOT) */
+  @DELETE
+  @Path("/{snapshotName}")
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(CORE_EDIT_PERM)
+  public DeleteSnapshotResponse deleteSnapshot(
+      @Parameter(
+              description = "The name of the core for which to delete a 
snapshot.",
+              required = true)
+          @PathParam("coreName")
+          String coreName,
+      @Parameter(description = "The name of the core snapshot to delete.", 
required = true)
+          @PathParam("snapshotName")
+          String snapshotName,
+      @Parameter(description = "The id to associate with the async task.") 
@QueryParam("async")
+          String taskId)
+      throws Exception {
+    final DeleteSnapshotResponse response = 
instantiateJerseyResponse(DeleteSnapshotResponse.class);
+
+    return handlePotentiallyAsynchronousTask(
+        response,
+        coreName,
+        taskId,
+        "deleteSnapshot",
+        () -> {
+          final SolrCore core = coreContainer.getCore(coreName);
+          if (core == null) {
+            throw new SolrException(
+                SolrException.ErrorCode.BAD_REQUEST, "Unable to locate core " 
+ coreName);
+          }
+
+          try {
+            try {
+              core.deleteNamedSnapshot(snapshotName);
+            } catch (IOException e) {
+              throw new CoreAdminAPIBaseException(e);
+            }
+
+            // Ideally we shouldn't need this. This is added since the RPC 
logic in
+            // OverseerCollectionMessageHandler can not provide the coreName 
as part of the result.
+            response.coreName = coreName;
+            response.commitName = snapshotName;
+          } finally {
+            core.close();
+          }
+
+          return response;
+        });
+  }
+
+  /** The Response for {@link CoreSnapshotAPI}'s {@link 
#deleteSnapshot(String, String, String)} */
+  public static class DeleteSnapshotResponse extends SolrJerseyResponse {
+    @Schema(description = "The name of the core.")
+    @JsonProperty(CoreAdminParams.CORE)
+    public String coreName;
+
+    @Schema(description = "The name of the deleted snapshot.")
+    @JsonProperty(CoreAdminParams.SNAPSHOT_NAME)
+    public String commitName;
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCoreSnapshots.java 
b/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCoreSnapshots.java
index ed4788f63c3..5f30ea1051c 100644
--- 
a/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCoreSnapshots.java
+++ 
b/solr/core/src/test/org/apache/solr/core/snapshots/TestSolrCoreSnapshots.java
@@ -310,20 +310,21 @@ public class TestSolrCoreSnapshots extends 
SolrCloudTestCase {
         snapshots.stream().filter(x -> 
commitName.equals(x.getName())).findFirst().isPresent());
   }
 
+  @SuppressWarnings("unchecked")
   private Collection<SnapshotMetaData> listSnapshots(SolrClient adminClient, 
String coreName)
       throws Exception {
     ListSnapshots req = new ListSnapshots();
     req.setCoreName(coreName);
     NamedList<?> resp = adminClient.request(req);
-    assertTrue(resp.get("snapshots") instanceof NamedList);
-    NamedList<?> apiResult = (NamedList<?>) resp.get("snapshots");
+    assertTrue(resp.get("snapshots") instanceof Map);
+    Map<String, Object> apiResult = (Map<String, Object>) 
resp.get("snapshots");
 
     List<SnapshotMetaData> result = new ArrayList<>(apiResult.size());
-    for (int i = 0; i < apiResult.size(); i++) {
-      String commitName = apiResult.getName(i);
-      String indexDirPath = (String) ((NamedList<?>) 
apiResult.get(commitName)).get("indexDirPath");
-      long genNumber =
-          Long.parseLong((String) ((NamedList<?>) 
apiResult.get(commitName)).get("generation"));
+    for (Map.Entry<String, Object> entry : apiResult.entrySet()) {
+      final String commitName = entry.getKey();
+      final String indexDirPath =
+          (String) ((Map<String, Object>) 
entry.getValue()).get("indexDirPath");
+      final long genNumber = (Long) ((Map<String, Object>) 
entry.getValue()).get("generation");
       result.add(new SnapshotMetaData(commitName, indexDirPath, genNumber));
     }
     return result;
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/StatsReloadRaceTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/StatsReloadRaceTest.java
index 3d48aa84319..7cdbf10f3e2 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/StatsReloadRaceTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/StatsReloadRaceTest.java
@@ -113,9 +113,9 @@ public class StatsReloadRaceTest extends SolrTestCaseJ4 {
 
     assertFalse(
         "expect status check w/o error, got:" + statusLog,
-        statusLog.contains(CoreAdminHandler.FAILED));
+        statusLog.contains(CoreAdminHandler.CoreAdminAsyncTracker.FAILED));
 
-    isCompleted = statusLog.contains(CoreAdminHandler.COMPLETED);
+    isCompleted = 
statusLog.contains(CoreAdminHandler.CoreAdminAsyncTracker.COMPLETED);
     return isCompleted;
   }
 
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/CoreSnapshotAPITest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CoreSnapshotAPITest.java
new file mode 100644
index 00000000000..7f3f566bd8b
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CoreSnapshotAPITest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
+
+  private CoreSnapshotAPI coreSnapshotAPI;
+
+  @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();
+
+    coreSnapshotAPI =
+        new CoreSnapshotAPI(
+            solrQueryRequest, solrQueryResponse, coreContainer, 
coreAdminAsyncTracker);
+  }
+
+  private List<String> snapshotsToCleanup = new ArrayList<>();
+
+  @After
+  public void deleteSnapshots() throws Exception {
+    for (String snapshotName : snapshotsToCleanup) {
+      coreSnapshotAPI.deleteSnapshot(coreName, snapshotName, null);
+    }
+
+    snapshotsToCleanup.clear();
+  }
+
+  @Test
+  public void testCreateSnapshotReturnsValidResponse() throws Exception {
+    final String snapshotName = "my-new-snapshot";
+
+    final CoreSnapshotAPI.CreateSnapshotResponse response =
+        coreSnapshotAPI.createSnapshot(coreName, snapshotName, null);
+    snapshotsToCleanup.add(snapshotName);
+
+    assertEquals(coreName, response.core);
+    assertEquals("my-new-snapshot", response.commitName);
+    assertNotNull(response.indexDirPath);
+    assertEquals(Long.valueOf(1L), response.generation);
+    assertFalse(response.files.isEmpty());
+  }
+
+  @Test
+  public void testReportsErrorWhenCreatingSnapshotForNonexistentCore() {
+    final String nonExistentCoreName = "non-existent";
+
+    final SolrException solrException =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              coreSnapshotAPI.createSnapshot(nonExistentCoreName, 
"my-new-snapshot", null);
+            });
+    assertEquals(400, solrException.code());
+    assertTrue(
+        "Exception message differed from expected: " + 
solrException.getMessage(),
+        solrException.getMessage().contains("Unable to locate core " + 
nonExistentCoreName));
+  }
+
+  @Test
+  public void testListSnapshotsReturnsValidResponse() throws Exception {
+    final String snapshotNameBase = "my-new-snapshot-";
+
+    for (int i = 0; i < 5; i++) {
+      final String snapshotName = snapshotNameBase + i;
+      coreSnapshotAPI.createSnapshot(coreName, snapshotName, null);
+      snapshotsToCleanup.add(snapshotName);
+    }
+
+    final CoreSnapshotAPI.ListSnapshotsResponse response = 
coreSnapshotAPI.listSnapshots(coreName);
+
+    assertEquals(5, response.snapshots.size());
+  }
+
+  @Test
+  public void testReportsErrorWhenListingSnapshotsForNonexistentCore() {
+    final String nonExistentCoreName = "non-existent";
+
+    final SolrException solrException =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              coreSnapshotAPI.listSnapshots(nonExistentCoreName);
+            });
+    assertEquals(400, solrException.code());
+    assertTrue(
+        "Exception message differed from expected: " + 
solrException.getMessage(),
+        solrException.getMessage().contains("Unable to locate core " + 
nonExistentCoreName));
+  }
+
+  @Test
+  public void testDeleteSnapshotReturnsValidResponse() throws Exception {
+    final String snapshotName = "my-new-snapshot";
+
+    coreSnapshotAPI.createSnapshot(coreName, snapshotName, null);
+
+    final CoreSnapshotAPI.DeleteSnapshotResponse deleteResponse =
+        coreSnapshotAPI.deleteSnapshot(coreName, snapshotName, null);
+
+    assertEquals(coreName, deleteResponse.coreName);
+    assertEquals(snapshotName, deleteResponse.commitName);
+
+    final CoreSnapshotAPI.ListSnapshotsResponse response = 
coreSnapshotAPI.listSnapshots(coreName);
+
+    assertEquals(0, response.snapshots.size());
+  }
+
+  @Test
+  public void testReportsErrorWhenDeletingSnapshotForNonexistentCore() {
+    final String nonExistentCoreName = "non-existent";
+
+    final SolrException solrException =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              coreSnapshotAPI.deleteSnapshot(nonExistentCoreName, 
"non-existent-snapshot", null);
+            });
+    assertEquals(400, solrException.code());
+    assertTrue(
+        "Exception message differed from expected: " + 
solrException.getMessage(),
+        solrException.getMessage().contains("Unable to locate core " + 
nonExistentCoreName));
+  }
+}
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
index 9ab7d8ed985..52224ed5d9b 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/backup-restore.adoc
@@ -234,38 +234,43 @@ 
http://localhost:8983/solr/gettingstarted/replication?command=restorestatus&wt=x
 The status value can be "In Progress", "success" or "failed".
 If it failed then an "exception" will also be sent in the response.
 
-=== Create Snapshot API
+[[create-snapshot-api]]
+== CREATE: Create a Snapshot
 
 The snapshot functionality is different from the backup functionality as the 
index files aren't copied anywhere.
 The index files are snapshotted in the same index directory and can be 
referenced while taking backups.
 
 You can trigger a snapshot command with an HTTP command like this (replace 
"techproducts" with the name of the core you are working with):
 
-.Create Snapshot API Example
-[source,text]
+[.dynamic-tabs]
+--
+[example.tab-pane#v1createsnapshot]
+====
+[.tab-label]*V1 API*
+
+[source,bash]
 ----
-http://localhost:8983/solr/admin/cores?action=CREATESNAPSHOT&core=techproducts&commitName=commit1
+curl -X POST 
http://localhost:8983/solr/admin/cores?action=CREATESNAPSHOT&core=techproducts&commitName=commit1
+
 ----
+====
 
-The `CREATESNAPSHOT` request parameters are:
+[example.tab-pane#v2createsnapshot]
+====
+[.tab-label]*V2 API*
 
-`commitName`::
-+
-[%autowidth,frame=none]
-|===
-|Optional |Default: none
-|===
-+
-The name to store the snapshot as.
+With the v2 API, the core and snapshot names are part of the path instead of 
query parameters.
 
-`core`::
-+
-[%autowidth,frame=none]
-|===
-|Optional |Default: none
-|===
-+
-The name of the core to perform the snapshot on.
+[source,bash]
+----
+curl -X POST http://localhost:8983/api/cores/techproducts/snapshots/commit1
+----
+====
+--
+
+=== CREATE Parameters
+
+The CREATE action allows the following parameters:
 
 `async`::
 +
@@ -276,28 +281,45 @@ The name of the core to perform the snapshot on.
 +
 Request ID to track this action which will be processed asynchronously.
 
-=== List Snapshot API
+=== CREATE Response
+
+The response will include the status of the request, the core name, snapshot 
name, index directory path, snapshot generation, and a list of file names.
+If the status is anything other than "success", an error message will explain 
why the request failed.
 
-The `LISTSNAPSHOTS` command lists all the taken snapshots for a particular 
core.
+[[list-snapshot-api]]
+== List: List All Snapshots for a Particular Core
 
 You can trigger a list snapshot command with an HTTP command like this 
(replace "techproducts" with the name of the core you are working with):
 
-.List Snapshot API
-[source,text]
+[.dynamic-tabs]
+--
+[example.tab-pane#v1listsnapshots]
+====
+[.tab-label]*V1 API*
+
+[source,bash]
 ----
-http://localhost:8983/solr/admin/cores?action=LISTSNAPSHOTS&core=techproducts&commitName=commit1
+curl 
http://localhost:8983/solr/admin/cores?action=LISTSNAPSHOTS&core=techproducts&commitName=commit1
+
 ----
+====
 
-The list snapshot request parameters are:
+[example.tab-pane#v2listsnapshots]
+====
+[.tab-label]*V2 API*
 
-`core`::
-+
-[%autowidth,frame=none]
-|===
-|Optional |Default: none
-|===
-+
-The name of the core to whose snapshots we want to list.
+With the v2 API the core name appears in the path, instead of as a query 
parameter.
+
+[source,bash]
+----
+curl http://localhost:8983/api/cores/techproducts/snapshots
+----
+====
+--
+
+=== LIST Parameters
+
+The LIST action allows the following parameters:
 
 `async`::
 +
@@ -308,37 +330,45 @@ The name of the core to whose snapshots we want to list.
 +
 Request ID to track this action which will be processed asynchronously.
 
-=== Delete Snapshot API
+=== LIST Response
+
+The response will include the status of the request and all existing snapshots 
for the core.
+If the status is anything other than "success", an error message will explain 
why the request failed.
 
-The `DELETESNAPSHOT` command deletes a snapshot for a particular core.
+[[delete-snapshot-api]]
+== DELETE: Delete a Snapshot
 
 You can trigger a delete snapshot with an HTTP command like this (replace 
"techproducts" with the name of the core you are working with):
 
-.Delete Snapshot API Example
-[source,text]
+[.dynamic-tabs]
+--
+[example.tab-pane#v1deletesnapshot]
+====
+[.tab-label]*V1 API*
+
+[source,bash]
 ----
-http://localhost:8983/solr/admin/cores?action=DELETESNAPSHOT&core=techproducts&commitName=commit1
+curl 
http://localhost:8983/solr/admin/cores?action=DELETESNAPSHOT&core=techproducts&commitName=commit1
+
 ----
+====
 
-The delete snapshot request parameters are:
+[example.tab-pane#v2deletesnapshot]
+====
+[.tab-label]*V2 API*
 
-`commitName`::
-+
-[%autowidth,frame=none]
-|===
-|Optional |Default: none
-|===
-+
-Specify the commit name to be deleted.
+With the v2 API, the core and snapshot names are part of the path instead of 
query parameters.
 
-`core`::
-+
-[%autowidth,frame=none]
-|===
-|Optional |Default: none
-|===
-+
-The name of the core whose snapshot we want to delete.
+[source,bash]
+----
+curl -X DELETE http://localhost:8983/api/cores/techproducts/snapshots/commit1
+----
+====
+--
+
+=== DELETE Parameters
+
+The DELETE action allows the following parameters:
 
 `async`::
 +
@@ -349,6 +379,11 @@ The name of the core whose snapshot we want to delete.
 +
 Request ID to track this action which will be processed asynchronously.
 
+=== DELETE Response
+
+The response will include the status of the request, the core name, and the 
name of the snapshot that was deleted.
+If the status is anything other than "success", an error message will explain 
why the request failed.
+
 == Backup/Restore Storage Repositories
 
 Solr provides a repository abstraction to allow users to backup and restore 
their data to a variety of different storage systems.
diff --git 
a/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java 
b/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
index 015d276d1cf..33d908ba4cc 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CoreAdminParams.java
@@ -144,6 +144,9 @@ public abstract class CoreAdminParams {
   /** A parameter to specify the name of the commit to be stored during the 
backup operation. */
   public static final String COMMIT_NAME = "commitName";
 
+  /** A parameter to specify the name of the snapshot to be stored during the 
backup operation. */
+  public static final String SNAPSHOT_NAME = "snapshotName";
+
   /** A boolean parameter specifying if a core is being created as part of a 
new collection */
   public static final String NEW_COLLECTION = "newCollection";
 

Reply via email to