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/05/16 15:53:39 UTC

[solr] branch branch_9x updated: SOLR-16392: Tweak v2 deletereplica to be more REST-ful (#1594)

This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new aaa718da892 SOLR-16392: Tweak v2 deletereplica to be more REST-ful (#1594)
aaa718da892 is described below

commit aaa718da892129c708b35b90ff6007b9d86d5a8b
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Wed May 10 08:55:38 2023 -0400

    SOLR-16392: Tweak v2 deletereplica to be more REST-ful (#1594)
    
    This commit tweaks the v2 binding for our "delete replica" API to be more
    intuitive for users.  Delete replica functionality now lives at:
      - `DELETE /api/collections/cName/shards/sName/replicas/rName`
      - `DELETE /api/collections/cName/shards/sName/replicas?count=3`
      - `PUT /api/collections/cName/scale {"count": 3"}
    
    depending on whether users want to delete a single replica by name,
    multiple replicas from a single shard, or multiple replicas from all shards
    respectively.  These v2 APIs are experimental and currently subject to
    change.
    
    This commit also switches them (and a similar deleteshard API) over to
    using JAX-RS.
---
 .../java/org/apache/solr/api/JerseyResource.java   |   8 +
 .../solr/handler/admin/CollectionsHandler.java     |  36 +--
 .../solr/handler/admin/api/AdminAPIBase.java       |  42 +++
 .../solr/handler/admin/api/DeleteReplicaAPI.java   | 281 ++++++++++++++++++---
 .../solr/handler/admin/api/DeleteShardAPI.java     | 118 +++++++--
 .../solr/handler/admin/TestApiFramework.java       |   7 -
 .../solr/handler/admin/TestCollectionAPIs.java     |  15 --
 .../handler/admin/api/DeleteReplicaAPITest.java    | 135 ++++++++++
 .../solr/handler/admin/api/DeleteShardAPITest.java |  93 +++++++
 .../handler/admin/api/V2ShardsAPIMappingTest.java  |  58 -----
 .../deployment-guide/pages/replica-management.adoc |  47 +++-
 .../deployment-guide/pages/shard-management.adoc   |  19 +-
 12 files changed, 679 insertions(+), 180 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/api/JerseyResource.java b/solr/core/src/java/org/apache/solr/api/JerseyResource.java
index 9ee9cf2ef44..e96216e170e 100644
--- a/solr/core/src/java/org/apache/solr/api/JerseyResource.java
+++ b/solr/core/src/java/org/apache/solr/api/JerseyResource.java
@@ -22,6 +22,7 @@ import static org.apache.solr.jersey.RequestContextKeys.SOLR_JERSEY_RESPONSE;
 import java.util.function.Supplier;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.core.Context;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.jersey.CatchAllExceptionMapper;
 import org.apache.solr.jersey.SolrJerseyResponse;
 import org.apache.solr.servlet.HttpSolrCall;
@@ -95,4 +96,11 @@ public class JerseyResource {
     }
     return instance;
   }
+
+  protected void ensureRequiredParameterProvided(String parameterName, Object parameterValue) {
+    if (parameterValue == null) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter: " + parameterName);
+    }
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index 4ddf73ae24c..499e58b4c3a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -26,7 +26,6 @@ import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREA
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREATE_NODE_SET_SHUFFLE;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.NUM_SLICES;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_ACTIVE_NODES;
-import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_IF_DOWN;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.SHARD_UNIQUE;
 import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
@@ -41,7 +40,6 @@ import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_TYPE;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static org.apache.solr.common.params.CollectionAdminParams.COUNT_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_NAME;
@@ -111,9 +109,6 @@ import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_PURGE_UNUSED;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
 import static org.apache.solr.common.params.CoreAdminParams.DATA_DIR;
-import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
-import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
-import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
 import static org.apache.solr.common.params.CoreAdminParams.INSTANCE_DIR;
 import static org.apache.solr.common.params.CoreAdminParams.MAX_NUM_BACKUP_POINTS;
 import static org.apache.solr.common.params.CoreAdminParams.ULOG_DIR;
@@ -769,16 +764,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     DELETESHARD_OP(
         DELETESHARD,
         (req, rsp, h) -> {
-          Map<String, Object> map =
-              copy(req.getParams().required(), null, COLLECTION_PROP, SHARD_ID_PROP);
-          copy(
-              req.getParams(),
-              map,
-              DELETE_INDEX,
-              DELETE_DATA_DIR,
-              DELETE_INSTANCE_DIR,
-              FOLLOW_ALIASES);
-          return map;
+          DeleteShardAPI.invokeWithV1Params(h.coreContainer, req, rsp);
+          return null;
         }),
     FORCELEADER_OP(
         FORCELEADER,
@@ -827,19 +814,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     DELETEREPLICA_OP(
         DELETEREPLICA,
         (req, rsp, h) -> {
-          Map<String, Object> map = copy(req.getParams().required(), null, COLLECTION_PROP);
-
-          return copy(
-              req.getParams(),
-              map,
-              DELETE_INDEX,
-              DELETE_DATA_DIR,
-              DELETE_INSTANCE_DIR,
-              COUNT_PROP,
-              REPLICA_PROP,
-              SHARD_ID_PROP,
-              ONLY_IF_DOWN,
-              FOLLOW_ALIASES);
+          DeleteReplicaAPI.invokeWithV1Params(h.coreContainer, req, rsp);
+          return null;
         }),
     MIGRATE_OP(
         MIGRATE,
@@ -1730,7 +1706,9 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         CreateCollectionBackupAPI.class,
         DeleteAliasAPI.class,
         DeleteCollectionAPI.class,
+        DeleteReplicaAPI.class,
         DeleteReplicaPropertyAPI.class,
+        DeleteShardAPI.class,
         InstallShardDataAPI.class,
         ListCollectionsAPI.class,
         ReplaceNodeAPI.class,
@@ -1750,10 +1728,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     apis.addAll(AnnotatedApi.getApis(new SplitShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new CreateShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new AddReplicaAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new DeleteShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SyncShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new ForceLeaderAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new DeleteReplicaAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new BalanceShardUniqueAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new MigrateDocsAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new ModifyCollectionAPI(this)));
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
index 0931ee7dfa7..ab2610afd74 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
@@ -17,10 +17,18 @@
 
 package org.apache.solr.handler.admin.api;
 
+import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+
+import java.util.Map;
 import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.logging.MDCLoggingContext;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -115,4 +123,38 @@ public abstract class AdminAPIBase extends JerseyResource {
   public void disableResponseCaching() {
     solrQueryResponse.setHttpCaching(false);
   }
+
+  protected SubResponseAccumulatingJerseyResponse submitRemoteMessageAndHandleResponse(
+      SubResponseAccumulatingJerseyResponse response,
+      CollectionParams.CollectionAction action,
+      ZkNodeProps remoteMessage,
+      String asyncId)
+      throws Exception {
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            action,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    if (asyncId != null) {
+      response.requestId = asyncId;
+    }
+
+    // Values fetched from remoteResponse may be null
+    response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
+    response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
+
+    return response;
+  }
+
+  protected static void insertIfNotNull(Map<String, Object> destination, String key, Object value) {
+    if (value != null) {
+      destination.put(key, value);
+    }
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaAPI.java
index 883b1a80bd1..fab78e3ac3a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaAPI.java
@@ -17,54 +17,265 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-import static org.apache.solr.common.params.CommonParams.ACTION;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_IF_DOWN;
+import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.COUNT_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
 import static org.apache.solr.common.params.CoreAdminParams.REPLICA;
-import static org.apache.solr.common.params.CoreAdminParams.SHARD;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.common.cloud.ZkStateReader;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 /**
- * V2 API for deleting an existing replica from a shard.
+ * V2 APIs for deleting one or more existing replicas from one or more shards.
  *
- * <p>This API (DELETE /v2/collections/collectionName/shards/shardName/replicaName is analogous to
- * the v1 /admin/collections?action=DELETEREPLICA command.
+ * <p>These APIs are analogous to the v1 /admin/collections?action=DELETEREPLICA command.
  */
-public class DeleteReplicaAPI {
+public class DeleteReplicaAPI extends AdminAPIBase {
 
-  private final CollectionsHandler collectionsHandler;
+  @Inject
+  public DeleteReplicaAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @DELETE
+  @Path("/collections/{collectionName}/shards/{shardName}/replicas/{replicaName}")
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse deleteReplicaByName(
+      @PathParam("collectionName") String collectionName,
+      @PathParam("shardName") String shardName,
+      @PathParam("replicaName") String replicaName,
+      // Optional params below
+      @QueryParam(FOLLOW_ALIASES) Boolean followAliases,
+      @QueryParam(DELETE_INSTANCE_DIR) Boolean deleteInstanceDir,
+      @QueryParam(DELETE_DATA_DIR) Boolean deleteDataDir,
+      @QueryParam(DELETE_INDEX) Boolean deleteIndex,
+      @QueryParam(ONLY_IF_DOWN) Boolean onlyIfDown,
+      @QueryParam(ASYNC) String asyncId)
+      throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+    ensureRequiredParameterProvided(SHARD_ID_PROP, shardName);
+    ensureRequiredParameterProvided(REPLICA, replicaName);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+    final ZkNodeProps remoteMessage =
+        createRemoteMessage(
+            collectionName,
+            shardName,
+            replicaName,
+            null,
+            followAliases,
+            deleteInstanceDir,
+            deleteDataDir,
+            deleteIndex,
+            onlyIfDown,
+            asyncId);
+    submitRemoteMessageAndHandleResponse(
+        response, CollectionParams.CollectionAction.DELETEREPLICA, remoteMessage, asyncId);
+    return response;
+  }
+
+  @DELETE
+  @Path("/collections/{collectionName}/shards/{shardName}/replicas")
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse deleteReplicasByCount(
+      @PathParam("collectionName") String collectionName,
+      @PathParam("shardName") String shardName,
+      @QueryParam(COUNT_PROP) Integer numToDelete,
+      // Optional params below
+      @QueryParam(FOLLOW_ALIASES) Boolean followAliases,
+      @QueryParam(DELETE_INSTANCE_DIR) Boolean deleteInstanceDir,
+      @QueryParam(DELETE_DATA_DIR) Boolean deleteDataDir,
+      @QueryParam(DELETE_INDEX) Boolean deleteIndex,
+      @QueryParam(ONLY_IF_DOWN) Boolean onlyIfDown,
+      @QueryParam(ASYNC) String asyncId)
+      throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+    ensureRequiredParameterProvided(SHARD_ID_PROP, shardName);
+    ensureRequiredParameterProvided(COUNT_PROP, numToDelete);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+    final ZkNodeProps remoteMessage =
+        createRemoteMessage(
+            collectionName,
+            shardName,
+            null,
+            numToDelete,
+            followAliases,
+            deleteInstanceDir,
+            deleteDataDir,
+            deleteIndex,
+            onlyIfDown,
+            asyncId);
+    submitRemoteMessageAndHandleResponse(
+        response, CollectionParams.CollectionAction.DELETEREPLICA, remoteMessage, asyncId);
+    return response;
+  }
+
+  public static class ScaleCollectionRequestBody implements JacksonReflectMapWriter {
+    public @JsonProperty(value = COUNT_PROP, required = true) Integer numToDelete;
+    public @JsonProperty(FOLLOW_ALIASES) Boolean followAliases;
+    public @JsonProperty(DELETE_INSTANCE_DIR) Boolean deleteInstanceDir;
+    public @JsonProperty(DELETE_DATA_DIR) Boolean deleteDataDir;
+    public @JsonProperty(DELETE_INDEX) Boolean deleteIndex;
+    public @JsonProperty(ONLY_IF_DOWN) Boolean onlyIfDown;
+    public @JsonProperty(ASYNC) String asyncId;
+
+    public static ScaleCollectionRequestBody fromV1Params(SolrParams v1Params) {
+      final var requestBody = new ScaleCollectionRequestBody();
+      requestBody.numToDelete = v1Params.getInt(COUNT_PROP);
+      requestBody.followAliases = v1Params.getBool(FOLLOW_ALIASES);
+      requestBody.deleteInstanceDir = v1Params.getBool(DELETE_INSTANCE_DIR);
+      requestBody.deleteDataDir = v1Params.getBool(DELETE_DATA_DIR);
+      requestBody.deleteIndex = v1Params.getBool(DELETE_INDEX);
+      requestBody.onlyIfDown = v1Params.getBool(ONLY_IF_DOWN);
+      requestBody.asyncId = v1Params.get(ASYNC);
+
+      return requestBody;
+    }
+  }
+
+  @PUT
+  @Path("/collections/{collectionName}/scale")
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse deleteReplicasByCountAllShards(
+      @PathParam("collectionName") String collectionName, ScaleCollectionRequestBody requestBody)
+      throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    if (requestBody == null) {
+      throw new SolrException(BAD_REQUEST, "Request body is required but missing");
+    }
+    ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+    ensureRequiredParameterProvided(COUNT_PROP, requestBody.numToDelete);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+    final ZkNodeProps remoteMessage =
+        createRemoteMessage(
+            collectionName,
+            null,
+            null,
+            requestBody.numToDelete,
+            requestBody.followAliases,
+            requestBody.deleteInstanceDir,
+            requestBody.deleteDataDir,
+            requestBody.deleteIndex,
+            requestBody.onlyIfDown,
+            requestBody.asyncId);
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.DELETEREPLICA,
+        remoteMessage,
+        requestBody.asyncId);
+    return response;
+  }
+
+  public static ZkNodeProps createRemoteMessage(
+      String collectionName,
+      String shardName,
+      String replicaName,
+      Integer numReplicasToDelete,
+      Boolean followAliases,
+      Boolean deleteInstanceDir,
+      Boolean deleteDataDir,
+      Boolean deleteIndex,
+      Boolean onlyIfDown,
+      String asyncId) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETEREPLICA.toLower());
+    remoteMessage.put(COLLECTION_PROP, collectionName);
+    insertIfNotNull(remoteMessage, SHARD_ID_PROP, shardName);
+    insertIfNotNull(remoteMessage, REPLICA, replicaName);
+    insertIfNotNull(remoteMessage, COUNT_PROP, numReplicasToDelete);
+    insertIfNotNull(remoteMessage, FOLLOW_ALIASES, followAliases);
+    insertIfNotNull(remoteMessage, DELETE_INSTANCE_DIR, deleteInstanceDir);
+    insertIfNotNull(remoteMessage, DELETE_DATA_DIR, deleteDataDir);
+    insertIfNotNull(remoteMessage, DELETE_INDEX, deleteIndex);
+    insertIfNotNull(remoteMessage, ONLY_IF_DOWN, onlyIfDown);
+    insertIfNotNull(remoteMessage, ASYNC, asyncId);
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static void invokeWithV1Params(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse)
+      throws Exception {
+    final var v1Params = solrQueryRequest.getParams();
+    v1Params.required().check(COLLECTION_PROP);
 
-  public DeleteReplicaAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
+    final var deleteReplicaApi =
+        new DeleteReplicaAPI(coreContainer, solrQueryRequest, solrQueryResponse);
+    final var v2Response = invokeApiMethod(deleteReplicaApi, v1Params);
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(solrQueryResponse, v2Response);
   }
 
-  @EndPoint(
-      path = {
-        "/c/{collection}/shards/{shard}/{replica}",
-        "/collections/{collection}/shards/{shard}/{replica}"
-      },
-      method = DELETE,
-      permission = COLL_EDIT_PERM)
-  public void deleteReplica(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    req =
-        wrapParams(
-            req,
-            ACTION,
-            CollectionParams.CollectionAction.DELETEREPLICA.toString(),
-            ZkStateReader.COLLECTION_PROP,
-            req.getPathTemplateValues().get(ZkStateReader.COLLECTION_PROP),
-            SHARD,
-            req.getPathTemplateValues().get(SHARD),
-            REPLICA,
-            req.getPathTemplateValues().get(REPLICA));
-
-    collectionsHandler.handleRequestBody(req, rsp);
+  private static SubResponseAccumulatingJerseyResponse invokeApiMethod(
+      DeleteReplicaAPI deleteReplicaApi, SolrParams v1Params) throws Exception {
+    if (v1Params.get(REPLICA) != null && v1Params.get(SHARD_ID_PROP) != null) {
+      return deleteReplicaApi.deleteReplicaByName(
+          v1Params.get(COLLECTION_PROP),
+          v1Params.get(SHARD_ID_PROP),
+          v1Params.get(REPLICA),
+          v1Params.getBool(FOLLOW_ALIASES),
+          v1Params.getBool(DELETE_INSTANCE_DIR),
+          v1Params.getBool(DELETE_DATA_DIR),
+          v1Params.getBool(DELETE_INDEX),
+          v1Params.getBool(ONLY_IF_DOWN),
+          v1Params.get(ASYNC));
+    } else if (v1Params.get(SHARD_ID_PROP) != null
+        && v1Params.get(COUNT_PROP) != null) { // Delete 'N' replicas
+      return deleteReplicaApi.deleteReplicasByCount(
+          v1Params.get(COLLECTION_PROP),
+          v1Params.get(SHARD_ID_PROP),
+          v1Params.getInt(COUNT_PROP),
+          v1Params.getBool(FOLLOW_ALIASES),
+          v1Params.getBool(DELETE_INSTANCE_DIR),
+          v1Params.getBool(DELETE_DATA_DIR),
+          v1Params.getBool(DELETE_INDEX),
+          v1Params.getBool(ONLY_IF_DOWN),
+          v1Params.get(ASYNC));
+    } else if (v1Params.get(COUNT_PROP) != null) {
+      return deleteReplicaApi.deleteReplicasByCountAllShards(
+          v1Params.get(COLLECTION_PROP), ScaleCollectionRequestBody.fromV1Params(v1Params));
+    } else {
+      throw new SolrException(
+          BAD_REQUEST,
+          "DELETEREPLICA requires either " + COUNT_PROP + " or " + REPLICA + " parameters");
+    }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java
index a5d05ac698d..3c2b6664308 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShardAPI.java
@@ -17,18 +17,29 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.common.params.CoreAdminParams.SHARD;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import java.util.HashMap;
 import java.util.Map;
-import org.apache.solr.api.EndPoint;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
@@ -38,25 +49,88 @@ import org.apache.solr.response.SolrQueryResponse;
  * <p>This API (DELETE /v2/collections/collectionName/shards/shardName) is analogous to the v1
  * /admin/collections?action=DELETESHARD command.
  */
-public class DeleteShardAPI {
-  private final CollectionsHandler collectionsHandler;
+@Path("/collections/{collectionName}/shards/{shardName}/replicas")
+public class DeleteShardAPI extends AdminAPIBase {
 
-  public DeleteShardAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
+  @Inject
+  public DeleteShardAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
-  @EndPoint(
-      path = {"/c/{collection}/shards/{shard}", "/collections/{collection}/shards/{shard}"},
-      method = DELETE,
-      permission = COLL_EDIT_PERM)
-  public void deleteShard(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    final Map<String, String> pathParams = req.getPathTemplateValues();
+  @DELETE
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse deleteShard(
+      @PathParam("collectionName") String collectionName,
+      @PathParam("shardName") String shardName,
+      @QueryParam(DELETE_INSTANCE_DIR) Boolean deleteInstanceDir,
+      @QueryParam(DELETE_DATA_DIR) Boolean deleteDataDir,
+      @QueryParam(DELETE_INDEX) Boolean deleteIndex,
+      @QueryParam(FOLLOW_ALIASES) Boolean followAliases,
+      @QueryParam(ASYNC) String asyncId)
+      throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+    ensureRequiredParameterProvided(SHARD_ID_PROP, shardName);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
 
-    final Map<String, Object> addedV1Params = new HashMap<>();
-    addedV1Params.put(ACTION, CollectionParams.CollectionAction.DELETESHARD.toLower());
-    addedV1Params.put(COLLECTION, pathParams.get(COLLECTION));
-    addedV1Params.put(SHARD, pathParams.get(SHARD));
+    final ZkNodeProps remoteMessage =
+        createRemoteMessage(
+            collectionName,
+            shardName,
+            deleteInstanceDir,
+            deleteDataDir,
+            deleteIndex,
+            followAliases,
+            asyncId);
+    submitRemoteMessageAndHandleResponse(
+        response, CollectionParams.CollectionAction.DELETESHARD, remoteMessage, asyncId);
+    return response;
+  }
+
+  public static ZkNodeProps createRemoteMessage(
+      String collectionName,
+      String shardName,
+      Boolean deleteInstanceDir,
+      Boolean deleteDataDir,
+      Boolean deleteIndex,
+      Boolean followAliases,
+      String asyncId) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETESHARD.toLower());
+    remoteMessage.put(COLLECTION_PROP, collectionName);
+    remoteMessage.put(SHARD_ID_PROP, shardName);
+    insertIfNotNull(remoteMessage, FOLLOW_ALIASES, followAliases);
+    insertIfNotNull(remoteMessage, DELETE_INSTANCE_DIR, deleteInstanceDir);
+    insertIfNotNull(remoteMessage, DELETE_DATA_DIR, deleteDataDir);
+    insertIfNotNull(remoteMessage, DELETE_INDEX, deleteIndex);
+    insertIfNotNull(remoteMessage, ASYNC, asyncId);
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static void invokeWithV1Params(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse)
+      throws Exception {
+    final var v1Params = solrQueryRequest.getParams();
+    v1Params.required().check(COLLECTION_PROP, SHARD_ID_PROP);
 
-    collectionsHandler.handleRequestBody(wrapParams(req, addedV1Params), rsp);
+    final var deleteShardApi =
+        new DeleteShardAPI(coreContainer, solrQueryRequest, solrQueryResponse);
+    final var v2Response =
+        deleteShardApi.deleteShard(
+            v1Params.get(COLLECTION_PROP),
+            v1Params.get(SHARD_ID_PROP),
+            v1Params.getBool(DELETE_INSTANCE_DIR),
+            v1Params.getBool(DELETE_DATA_DIR),
+            v1Params.getBool(DELETE_INDEX),
+            v1Params.getBool(FOLLOW_ALIASES),
+            v1Params.get(ASYNC));
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(solrQueryResponse, v2Response);
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
index 02dc1cf7df8..a04e4bc4aef 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
@@ -121,13 +121,6 @@ public class TestApiFramework extends SolrTestCaseJ4 {
     assertConditions(api.getSpec(), Map.of("/methods[0]", "POST", "/commands/modify", NOT_NULL));
     assertEquals("hello", parts.get("collection"));
 
-    api =
-        V2HttpCall.getApiInfo(
-            containerHandlers, "/collections/hello/shards/shard1/replica1", "DELETE", null, parts);
-    assertEquals("hello", parts.get("collection"));
-    assertEquals("shard1", parts.get("shard"));
-    assertEquals("replica1", parts.get("replica"));
-
     SolrQueryResponse rsp = invoke(containerHandlers, null, "/collections/_introspect", mockCC);
 
     Set<String> methodNames = new HashSet<>();
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
index 611b7bd9d66..1411452d66a 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
 import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_NAME;
@@ -92,20 +91,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
     compareOutput(
         apiBag, "/collections/collName", POST, "{reload:{}}", "{name:collName, operation :reload}");
 
-    compareOutput(
-        apiBag,
-        "/collections/collName/shards/shard1",
-        DELETE,
-        null,
-        "{collection:collName, shard: shard1 , operation :deleteshard }");
-
-    compareOutput(
-        apiBag,
-        "/collections/collName/shards/shard1/replica1?deleteDataDir=true&onlyIfDown=true",
-        DELETE,
-        null,
-        "{collection:collName, shard: shard1, replica :replica1 , deleteDataDir:'true', onlyIfDown: 'true', operation :deletereplica }");
-
     compareOutput(
         apiBag,
         "/collections/collName/shards",
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaAPITest.java
new file mode 100644
index 00000000000..3c56f7d189f
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaAPITest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_IF_DOWN;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CollectionAdminParams.COUNT_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CollectionAdminParams.REPLICA;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteReplicaAPI} */
+public class DeleteReplicaAPITest extends SolrTestCaseJ4 {
+  @Test
+  public void testReportsErrorIfCollectionNameMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new DeleteReplicaAPI(null, null, null);
+              api.deleteReplicaByName(
+                  null, "someShard", "someReplica", null, null, null, null, null, null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: collection", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfShardNameMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new DeleteReplicaAPI(null, null, null);
+              api.deleteReplicaByName(
+                  "someCollection", null, "someReplica", null, null, null, null, null, null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: shard", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfReplicaNameMissingWhenDeletingByName() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new DeleteReplicaAPI(null, null, null);
+              api.deleteReplicaByName(
+                  "someCollection", "someShard", null, null, null, null, null, null, null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: replica", thrown.getMessage());
+  }
+
+  @Test
+  public void testCreateRemoteMessageAllProperties() {
+    final var remoteMessage =
+        DeleteReplicaAPI.createRemoteMessage(
+                "someCollName",
+                "someShardName",
+                "someReplicaName",
+                123,
+                true,
+                false,
+                true,
+                false,
+                true,
+                "someAsyncId")
+            .getProperties();
+
+    assertEquals(11, remoteMessage.size());
+    assertEquals("deletereplica", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someCollName", remoteMessage.get(COLLECTION));
+    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+    assertEquals("someReplicaName", remoteMessage.get(REPLICA));
+    assertEquals(Integer.valueOf(123), remoteMessage.get(COUNT_PROP));
+    assertEquals(Boolean.TRUE, remoteMessage.get(FOLLOW_ALIASES));
+    assertEquals(Boolean.FALSE, remoteMessage.get(DELETE_INSTANCE_DIR));
+    assertEquals(Boolean.TRUE, remoteMessage.get(DELETE_DATA_DIR));
+    assertEquals(Boolean.FALSE, remoteMessage.get(DELETE_INDEX));
+    assertEquals(Boolean.TRUE, remoteMessage.get(ONLY_IF_DOWN));
+    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+  }
+
+  @Test
+  public void testMissingValuesExcludedFromRemoteMessage() {
+    final var remoteMessage =
+        DeleteReplicaAPI.createRemoteMessage(
+                "someCollName",
+                "someShardName",
+                "someReplicaName",
+                null,
+                null,
+                null,
+                null,
+                null,
+                null,
+                null)
+            .getProperties();
+
+    assertEquals(4, remoteMessage.size());
+    assertEquals("deletereplica", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someCollName", remoteMessage.get(COLLECTION));
+    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+    assertEquals("someReplicaName", remoteMessage.get(REPLICA));
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteShardAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteShardAPITest.java
new file mode 100644
index 00000000000..c821bcb68cc
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteShardAPITest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteShardAPI} */
+public class DeleteShardAPITest extends SolrTestCaseJ4 {
+  @Test
+  public void testReportsErrorIfCollectionNameMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new DeleteShardAPI(null, null, null);
+              api.deleteShard(null, "someShard", null, null, null, null, null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: collection", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfShardNameMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new DeleteShardAPI(null, null, null);
+              api.deleteShard("someCollection", null, null, null, null, null, null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: shard", thrown.getMessage());
+  }
+
+  @Test
+  public void testCreateRemoteMessageAllProperties() {
+    final var remoteMessage =
+        DeleteShardAPI.createRemoteMessage(
+                "someCollName", "someShardName", true, false, true, false, "someAsyncId")
+            .getProperties();
+
+    assertEquals(8, remoteMessage.size());
+    assertEquals("deleteshard", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someCollName", remoteMessage.get(COLLECTION));
+    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+    assertEquals(Boolean.TRUE, remoteMessage.get(DELETE_INSTANCE_DIR));
+    assertEquals(Boolean.FALSE, remoteMessage.get(DELETE_DATA_DIR));
+    assertEquals(Boolean.TRUE, remoteMessage.get(DELETE_INDEX));
+    assertEquals(Boolean.FALSE, remoteMessage.get(FOLLOW_ALIASES));
+    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+  }
+
+  @Test
+  public void testMissingValuesExcludedFromRemoteMessage() {
+    final var remoteMessage =
+        DeleteShardAPI.createRemoteMessage(
+                "someCollName", "someShardName", null, null, null, null, null)
+            .getProperties();
+
+    assertEquals(3, remoteMessage.size());
+    assertEquals("deleteshard", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someCollName", remoteMessage.get(COLLECTION));
+    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
index 96889898293..af4fda1cd43 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
@@ -17,14 +17,12 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_IF_DOWN;
 import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
 import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static org.apache.solr.common.params.CollectionAdminParams.COUNT_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionParams.NAME;
@@ -37,16 +35,10 @@ import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.apache.solr.common.params.CommonParams.ACTION;
 import static org.apache.solr.common.params.CommonParams.TIMING;
-import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
-import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
-import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
-import static org.apache.solr.common.params.CoreAdminParams.REPLICA;
 import static org.apache.solr.common.params.CoreAdminParams.SHARD;
 
-import java.util.Locale;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CoreAdminParams;
-import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.handler.admin.V2ApiMappingTest;
@@ -69,10 +61,8 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
     apiBag.registerObject(new SplitShardAPI(collectionsHandler));
     apiBag.registerObject(new CreateShardAPI(collectionsHandler));
     apiBag.registerObject(new AddReplicaAPI(collectionsHandler));
-    apiBag.registerObject(new DeleteShardAPI(collectionsHandler));
     apiBag.registerObject(new SyncShardAPI(collectionsHandler));
     apiBag.registerObject(new ForceLeaderAPI(collectionsHandler));
-    apiBag.registerObject(new DeleteReplicaAPI(collectionsHandler));
   }
 
   @Override
@@ -107,25 +97,6 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
     assertEquals("shardName", v1Params.get(SHARD));
   }
 
-  @Test
-  public void testDeleteShardAllProperties() throws Exception {
-    final ModifiableSolrParams v2QueryParams = new ModifiableSolrParams();
-    v2QueryParams.add("deleteIndex", "true");
-    v2QueryParams.add("deleteDataDir", "true");
-    v2QueryParams.add("deleteInstanceDir", "true");
-    v2QueryParams.add("followAliases", "true");
-    final SolrParams v1Params =
-        captureConvertedV1Params("/collections/collName/shards/shardName", "DELETE", v2QueryParams);
-
-    assertEquals(CollectionParams.CollectionAction.DELETESHARD.lowerName, v1Params.get(ACTION));
-    assertEquals("collName", v1Params.get(COLLECTION));
-    assertEquals("shardName", v1Params.get(SHARD));
-    assertEquals("true", v1Params.get("deleteIndex"));
-    assertEquals("true", v1Params.get("deleteDataDir"));
-    assertEquals("true", v1Params.get("deleteInstanceDir"));
-    assertEquals("true", v1Params.get("followAliases"));
-  }
-
   @Test
   public void testSplitShardAllProperties() throws Exception {
     final SolrParams v1Params =
@@ -245,33 +216,4 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
     assertEquals("foo1", v1Params.get("property.foo"));
     assertEquals("bar1", v1Params.get("property.bar"));
   }
-
-  // really this is a replica API, but since there's only 1 API on the replica path, it's included
-  // here for simplicity.
-  @Test
-  public void testDeleteReplicaAllProperties() throws Exception {
-    final ModifiableSolrParams v2QueryParams = new ModifiableSolrParams();
-    v2QueryParams.add("deleteIndex", "true");
-    v2QueryParams.add("deleteDataDir", "true");
-    v2QueryParams.add("deleteInstanceDir", "true");
-    v2QueryParams.add("followAliases", "true");
-    v2QueryParams.add("count", "4");
-    v2QueryParams.add("onlyIfDown", "true");
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/collections/collName/shards/shard1/someReplica", "DELETE", v2QueryParams);
-
-    assertEquals(
-        CollectionParams.CollectionAction.DELETEREPLICA.lowerName,
-        v1Params.get(ACTION).toLowerCase(Locale.ROOT));
-    assertEquals("collName", v1Params.get(COLLECTION));
-    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
-    assertEquals("someReplica", v1Params.get(REPLICA));
-    assertEquals("true", v1Params.get(DELETE_INDEX));
-    assertEquals("true", v1Params.get(DELETE_DATA_DIR));
-    assertEquals("true", v1Params.get(DELETE_INSTANCE_DIR));
-    assertEquals("true", v1Params.get(FOLLOW_ALIASES));
-    assertEquals("4", v1Params.get(COUNT_PROP));
-    assertEquals("true", v1Params.get(ONLY_IF_DOWN));
-  }
 }
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/replica-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/replica-management.adoc
index 0123679fc91..39667652845 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/replica-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/replica-management.adoc
@@ -451,10 +451,16 @@ Request ID to track this action which will be xref:configuration-guide:collectio
 [[deletereplica]]
 == DELETEREPLICA: Delete a Replica
 
-Deletes a named replica from the specified collection and shard.
+Allows deletion of one or more replicas.  The replicas to be deleted can be specified in multiple ways:
 
-If the corresponding core is up and running the core is unloaded, the entry is removed from the clusterstate, and (by default) delete the instanceDir and dataDir.
-If the node/core is down, the entry is taken off the clusterstate and if the core comes up later it is automatically unregistered.
+1. A single, specific replica can be deleted by if the associated collection, shard and replica name are all provided.
+2. Multiple replicas can be deleted from a specific shard if the associated collection and shard names are provided, along with a `count` of the replicas to delete.
+3. Multiple replicas can be deleted from _all_ shards in a collection if the associated collection name is provided, along with a `count` of the replicas to delete.
+
+When deleting multiple replicas, Solr chooses replicas which are active, up to date, and not currently the leader.
+
+For each replica being deleted, if the corresponding core is up and running the core is unloaded, the entry is removed from the clusterstate, and (by default) the instanceDir and dataDir are deleted.
+If the core underlying the replica is down, the entry is taken off the clusterstate and if the core comes up later it is automatically unregistered.
 
 [.dynamic-tabs]
 --
@@ -472,17 +478,32 @@ http://localhost:8983/solr/admin/collections?action=DELETEREPLICA&collection=tec
 ====
 [.tab-label]*V2 API*
 
+The v2 API has three distinct endpoints for replica-deletion, depending on how the replicas are specified.
+
+To delete a replica by name:
+
+
+[source,bash]
+----
+curl -X DELETE http://localhost:8983/api/collections/techproducts/shards/shard1/replicas/core_node2
+----
+
+To delete a specified number of (unnamed) replicas from a single shard:
 
 [source,bash]
 ----
-curl -X DELETE http://localhost:8983/api/collections/techproducts/shards/shard1/core_node2
+curl -X DELETE "http://localhost:8983/api/collections/techproducts/shards/shard1/replicas?count=3"
 ----
 
-To run a DELETE asynchronously then append the `async` parameter:
+To delete a specified number of (unnamed) replicas from all shards:
 
 [source,bash]
 ----
-curl -X DELETE http://localhost:8983/api/collections/techproducts/shards/shard1/core_node2?async=aaaa
+curl -X PUT -H "Content-type: application/json" "http://localhost:8983/api/collections/techproducts/scale" -d '
+  {
+    "count": 3
+  }
+'
 ----
 ====
 --
@@ -497,6 +518,8 @@ s|Required |Default: none
 |===
 +
 The name of the collection.
+Provided as a query parameter or a path parameter in v1 and v2 requests, respectively.
+
 
 `shard`::
 +
@@ -506,6 +529,8 @@ s|Required |Default: none
 |===
 +
 The name of the shard that includes the replica to be removed.
+Provided as a query parameter or a path parameter in v1 and v2 requests, respectively.
+
 
 `replica`::
 +
@@ -515,6 +540,7 @@ The name of the shard that includes the replica to be removed.
 |===
 +
 The name of the replica to remove.
+Provided as a query parameter or a path parameter in v1 and v2 requests, respectively.
 +
 If `count` is used instead, this parameter is not required.
 Otherwise, this parameter must be supplied.
@@ -572,6 +598,15 @@ Set this to `false` to prevent the index directory from being deleted.
 +
 When set to `true`, no action will be taken if the replica is active.
 
+`followAliases`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: false
+|===
++
+A flag that allows treating the collection parameter as an alias for the actual collection name to be resolved.
+
 `async`::
 +
 [%autowidth,frame=none]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
index 2d5c46fcb77..a691743c1e5 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
@@ -473,13 +473,6 @@ http://localhost:8983/solr/admin/collections?action=DELETESHARD&shard=shard1&col
 ----
 curl -X DELETE http://localhost:8983/api/collections/techproducts/shards/shard1
 ----
-
-To run a DELETE asynchronously then append the `async` parameter:
-
-[source,bash]
-----
-curl -X DELETE http://localhost:8983/api/collections/techproducts/shards/shard1?async=aaaa
-----
 ====
 --
 
@@ -493,6 +486,7 @@ s|Required |Default: none
 |===
 +
 The name of the collection that includes the shard to be deleted.
+Provided as a query parameter or a path parameter in v1 and v2 requests, respectively.
 
 `shard`::
 +
@@ -502,6 +496,8 @@ s|Required |Default: none
 |===
 +
 The name of the shard to be deleted.
+Provided as a query parameter or a path parameter in v1 and v2 requests, respectively.
+
 
 `deleteInstanceDir`::
 +
@@ -533,6 +529,15 @@ Set this to `false` to prevent the data directory from being deleted.
 By default Solr will delete the index of each replica that is deleted.
 Set this to `false` to prevent the index directory from being deleted.
 
+`followAliases`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: false
+|===
++
+A flag that allows treating the collection parameter as an alias for the actual collection name to be resolved.
+
 `async`::
 +
 [%autowidth,frame=none]