You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ho...@apache.org on 2026/01/16 22:20:08 UTC

(solr) branch main updated: SOLR-18072: Refactor CollectionApiCommands to add expandable context (#4046)

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

houston 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 ec0c988fab9 SOLR-18072: Refactor CollectionApiCommands to add expandable context (#4046)
ec0c988fab9 is described below

commit ec0c988fab94904e25f80524c30ce8eeb4107003
Author: Houston Putman <ho...@apache.org>
AuthorDate: Fri Jan 16 14:20:02 2026 -0800

    SOLR-18072: Refactor CollectionApiCommands to add expandable context (#4046)
---
 .../solr-18072-refactor-admin-cmd-apis.yml         |  11 ++
 gradle/validation/validate-source-patterns.gradle  |   2 +-
 .../client/api/endpoint/AliasPropertyApis.java     |   3 +-
 .../client/api/endpoint/BalanceReplicasApi.java    |   4 +-
 .../solr/client/api/endpoint/CreateAliasApi.java   |   4 +-
 .../solr/client/api/endpoint/DeleteAliasApi.java   |   4 +-
 .../solr/client/api/endpoint/DeleteNodeApi.java    |   4 +-
 .../client/api/endpoint/InstallShardDataApi.java   |   4 +-
 .../client/api/endpoint/MigrateReplicasApi.java    |   4 +-
 .../solr/client/api/endpoint/ReplaceNodeApi.java   |   4 +-
 .../model/CreateCollectionSnapshotResponse.java    |   2 +-
 .../model/DeleteCollectionSnapshotResponse.java    |   2 +-
 .../solr/cloud/api/collections/AddReplicaCmd.java  |  19 +-
 .../cloud/api/collections/AdminCmdContext.java     |  63 +++++++
 .../solr/cloud/api/collections/AliasCmd.java       |  10 +-
 .../solr/cloud/api/collections/BackupCmd.java      |  28 ++-
 .../cloud/api/collections/BalanceReplicasCmd.java  |   8 +-
 .../solr/cloud/api/collections/CollApiCmds.java    |  86 ++++-----
 .../api/collections/CollectionApiLockFactory.java  |   7 +-
 .../api/collections/CollectionCommandContext.java  |   7 +-
 .../api/collections/CollectionHandlingUtils.java   |  49 +++---
 .../solr/cloud/api/collections/CreateAliasCmd.java |  20 ++-
 .../cloud/api/collections/CreateCollectionCmd.java |  34 ++--
 .../solr/cloud/api/collections/CreateShardCmd.java |  31 ++--
 .../cloud/api/collections/CreateSnapshotCmd.java   |   7 +-
 .../solr/cloud/api/collections/DeleteAliasCmd.java |   3 +-
 .../cloud/api/collections/DeleteBackupCmd.java     |   3 +-
 .../cloud/api/collections/DeleteCollectionCmd.java |  10 +-
 .../solr/cloud/api/collections/DeleteNodeCmd.java  |  13 +-
 .../cloud/api/collections/DeleteReplicaCmd.java    |  26 +--
 .../solr/cloud/api/collections/DeleteShardCmd.java |  14 +-
 .../cloud/api/collections/DeleteSnapshotCmd.java   |   7 +-
 ...istributedCollectionConfigSetCommandRunner.java |  96 +++++-----
 .../cloud/api/collections/InstallShardDataCmd.java |   5 +-
 .../api/collections/MaintainRoutedAliasCmd.java    |  33 ++--
 .../solr/cloud/api/collections/MigrateCmd.java     |  84 +++++----
 .../cloud/api/collections/MigrateReplicasCmd.java  |   7 +-
 .../solr/cloud/api/collections/MoveReplicaCmd.java |  89 ++++++----
 .../OverseerCollectionMessageHandler.java          |   5 +-
 .../cloud/api/collections/OverseerRoleCmd.java     |   3 +-
 .../cloud/api/collections/OverseerStatusCmd.java   |   3 +-
 .../api/collections/ReindexCollectionCmd.java      | 111 +++++++-----
 .../solr/cloud/api/collections/RenameCmd.java      |   5 +-
 .../solr/cloud/api/collections/ReplaceNodeCmd.java |   7 +-
 .../api/collections/ReplicaMigrationUtils.java     |  31 ++--
 .../solr/cloud/api/collections/RestoreCmd.java     | 109 +++++++-----
 .../cloud/api/collections/SetAliasPropCmd.java     |   3 +-
 .../solr/cloud/api/collections/SplitShardCmd.java  |  66 ++++---
 .../solr/handler/admin/CollectionsHandler.java     |  39 +++--
 .../solr/handler/admin/RebalanceLeaders.java       |  12 +-
 .../solr/handler/admin/api/AddReplicaProperty.java |  18 +-
 .../solr/handler/admin/api/AdminAPIBase.java       |  67 +++++--
 .../solr/handler/admin/api/AliasProperty.java      |  79 ++++-----
 .../solr/handler/admin/api/BalanceReplicas.java    |  34 ++--
 .../solr/handler/admin/api/BalanceShardUnique.java |   7 +-
 .../apache/solr/handler/admin/api/CreateAlias.java |  31 +---
 .../solr/handler/admin/api/CreateCollection.java   |  32 +---
 .../handler/admin/api/CreateCollectionBackup.java  |  21 +--
 .../admin/api/CreateCollectionSnapshot.java        |  30 +---
 .../solr/handler/admin/api/CreateReplica.java      |  10 +-
 .../apache/solr/handler/admin/api/CreateShard.java |   9 +-
 .../apache/solr/handler/admin/api/DeleteAlias.java |  44 +----
 .../solr/handler/admin/api/DeleteCollection.java   |  39 +----
 .../handler/admin/api/DeleteCollectionBackup.java  |  19 +-
 .../admin/api/DeleteCollectionSnapshot.java        |  29 +--
 .../apache/solr/handler/admin/api/DeleteNode.java  |  43 ++---
 .../solr/handler/admin/api/DeleteReplica.java      |  15 +-
 .../handler/admin/api/DeleteReplicaProperty.java   |  23 +--
 .../apache/solr/handler/admin/api/DeleteShard.java |   9 +-
 .../solr/handler/admin/api/InstallShardData.java   |  30 +---
 .../solr/handler/admin/api/MigrateReplicas.java    |  34 ++--
 .../handler/admin/api/ReloadCollectionAPI.java     |  17 +-
 .../solr/handler/admin/api/RenameCollection.java   |   8 +-
 .../apache/solr/handler/admin/api/ReplaceNode.java |  35 ++--
 .../solr/handler/admin/api/RestoreCollection.java  |  32 +---
 .../src/java/org/apache/solr/util/TimeOut.java     |   9 +
 .../OverseerCollectionConfigSetProcessorTest.java  |  27 ++-
 .../api/collections/CollectionApiLockingTest.java  |  40 ++++-
 .../admin/api/AddReplicaPropertyAPITest.java       |  81 ++-------
 .../solr/handler/admin/api/BackupCoreAPITest.java  |  25 +--
 .../admin/api/BalanceShardUniqueAPITest.java       |  64 +++----
 .../handler/admin/api/CoreSnapshotAPITest.java     |  30 ++--
 .../solr/handler/admin/api/CreateAliasAPITest.java | 141 ++++++++-------
 .../handler/admin/api/CreateCollectionAPITest.java | 112 ++++++------
 .../admin/api/CreateCollectionSnapshotAPITest.java |  90 ++++++----
 .../handler/admin/api/CreateReplicaAPITest.java    |  94 +++++-----
 .../solr/handler/admin/api/CreateShardAPITest.java |  95 +++++-----
 .../solr/handler/admin/api/DeleteAliasAPITest.java | 117 +++++++++++--
 .../handler/admin/api/DeleteCollectionAPITest.java |  69 ++++----
 .../admin/api/DeleteCollectionBackupAPITest.java   | 148 +++++++++-------
 .../admin/api/DeleteCollectionSnapshotAPITest.java |  84 +++++----
 .../solr/handler/admin/api/DeleteNodeAPITest.java  | 194 ++++++++++++++++-----
 .../handler/admin/api/DeleteReplicaAPITest.java    | 175 ++++++++++++-------
 .../admin/api/DeleteReplicaPropertyAPITest.java    |  71 ++++----
 .../solr/handler/admin/api/DeleteShardAPITest.java |  79 +++++----
 .../solr/handler/admin/api/ForceLeaderAPITest.java |  32 ++--
 .../handler/admin/api/MigrateReplicasAPITest.java  |  88 +++-------
 .../solr/handler/admin/api/MockV2APITest.java      | 123 +++++++++++++
 .../handler/admin/api/ReloadCollectionAPITest.java |  43 +++--
 .../solr/handler/admin/api/ReloadCoreAPITest.java  |  11 +-
 .../solr/handler/admin/api/ReplaceNodeAPITest.java | 110 ++++--------
 .../admin/api/RestoreCollectionAPITest.java        | 146 ++++++++--------
 .../solr/handler/admin/api/SyncShardAPITest.java   |  32 ++--
 .../solr/handler/admin/api/UnloadCoreAPITest.java  |  11 +-
 .../admin/api/V2CollectionBackupApiTest.java       |  98 +++++++----
 .../client/solrj/response/RequestStatusState.java  |  19 +-
 .../org/apache/solr/common/util/RetryUtil.java     |  45 +++++
 .../src/java/org/apache/solr/SolrTestCaseJ4.java   |  16 ++
 .../org/apache/solr/cloud/SolrCloudTestCase.java   |   7 +
 109 files changed, 2414 insertions(+), 2028 deletions(-)

diff --git a/changelog/unreleased/solr-18072-refactor-admin-cmd-apis.yml b/changelog/unreleased/solr-18072-refactor-admin-cmd-apis.yml
new file mode 100644
index 00000000000..6bec91c16cc
--- /dev/null
+++ b/changelog/unreleased/solr-18072-refactor-admin-cmd-apis.yml
@@ -0,0 +1,11 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Refactor CollectionApiCommands to add easily expandable AdminCmdContext argument
+type: other # added, changed, fixed, deprecated, removed, dependency_update, security, other
+authors:
+  - name: Houston Putman
+    nick: HoustonPutman
+  - name: David Smiley
+  - name: Jason Gerlowski
+links:
+  - name: SOLR-18072
+    url: https://issues.apache.org/jira/browse/SOLR-18072
diff --git a/gradle/validation/validate-source-patterns.gradle b/gradle/validation/validate-source-patterns.gradle
index a164dd75086..aeaca6c97e9 100644
--- a/gradle/validation/validate-source-patterns.gradle
+++ b/gradle/validation/validate-source-patterns.gradle
@@ -209,7 +209,7 @@ class ValidateSourcePatternsTask extends DefaultTask {
     }
 
     def checkMockitoAssume = {f, text ->
-      if (text.contains("mockito") && !text.contains("assumeWorkingMockito()")) {
+      if (text.contains("mockito") && !(text.contains("assumeWorkingMockito()") || text.contains("extends MockV2APITest"))) {
         reportViolation(f, 'File uses Mockito but has no assumeWorkingMockito() call')
       }
     }
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/AliasPropertyApis.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/AliasPropertyApis.java
index 2c1f2ae8267..fa881d087f1 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/AliasPropertyApis.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/AliasPropertyApis.java
@@ -24,6 +24,7 @@ import jakarta.ws.rs.GET;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
 import org.apache.solr.client.api.model.GetAliasPropertyResponse;
 import org.apache.solr.client.api.model.GetAllAliasPropertiesResponse;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
@@ -56,7 +57,7 @@ public interface AliasPropertyApis {
   @Operation(
       summary = "Update properties for a collection alias.",
       tags = {"alias-properties"})
-  SolrJerseyResponse updateAliasProperties(
+  AsyncJerseyResponse updateAliasProperties(
       @Parameter(description = "Alias Name") @PathParam("aliasName") String aliasName,
       @RequestBody(description = "Properties that need to be updated", required = true)
           UpdateAliasPropertiesRequestBody requestBody)
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java
index 9cbfa5bf3a8..3fd863d1e0a 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java
@@ -21,7 +21,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
 import org.apache.solr.client.api.model.BalanceReplicasRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 
 @Path("cluster/replicas/balance")
 public interface BalanceReplicasApi {
@@ -29,7 +29,7 @@ public interface BalanceReplicasApi {
   @Operation(
       summary = "Balance Replicas across the given set of Nodes.",
       tags = {"cluster"})
-  SolrJerseyResponse balanceReplicas(
+  SubResponseAccumulatingJerseyResponse balanceReplicas(
       @RequestBody(description = "Contains user provided parameters")
           BalanceReplicasRequestBody requestBody)
       throws Exception;
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/CreateAliasApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/CreateAliasApi.java
index 78b9b4376c0..553e34f9fe5 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/CreateAliasApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/CreateAliasApi.java
@@ -19,8 +19,8 @@ package org.apache.solr.client.api.endpoint;
 import io.swagger.v3.oas.annotations.Operation;
 import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
 import org.apache.solr.client.api.model.CreateAliasRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
 
 @Path("/aliases")
 public interface CreateAliasApi {
@@ -28,5 +28,5 @@ public interface CreateAliasApi {
   @Operation(
       summary = "Create a traditional or 'routed' alias",
       tags = {"aliases"})
-  SolrJerseyResponse createAlias(CreateAliasRequestBody requestBody) throws Exception;
+  AsyncJerseyResponse createAlias(CreateAliasRequestBody requestBody) throws Exception;
 }
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteAliasApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteAliasApi.java
index badaea3c117..082760f9f41 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteAliasApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteAliasApi.java
@@ -23,7 +23,7 @@ import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import jakarta.ws.rs.QueryParam;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
 
 @Path("/aliases/{aliasName}")
 public interface DeleteAliasApi {
@@ -32,7 +32,7 @@ public interface DeleteAliasApi {
   @Operation(
       summary = "Deletes an alias by its name",
       tags = {"aliases"})
-  SolrJerseyResponse deleteAlias(
+  AsyncJerseyResponse deleteAlias(
       @Parameter(description = "The name of the alias to delete", required = true)
           @PathParam("aliasName")
           String aliasName,
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteNodeApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteNodeApi.java
index 64b5978afd2..b42491b335f 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteNodeApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/DeleteNodeApi.java
@@ -24,7 +24,7 @@ import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import org.apache.solr.client.api.model.DeleteNodeRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 
 @Path("cluster/nodes/{nodeName}/clear/")
 public interface DeleteNodeApi {
@@ -33,7 +33,7 @@ public interface DeleteNodeApi {
   @Operation(
       summary = "Delete all replicas off of the specified SolrCloud node",
       tags = {"node"})
-  SolrJerseyResponse deleteNode(
+  SubResponseAccumulatingJerseyResponse deleteNode(
       @Parameter(
               description =
                   "The name of the node to be cleared.  Usually of the form 'host:1234_solr'.",
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/InstallShardDataApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/InstallShardDataApi.java
index f38c2bd00ee..e91c57054bf 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/InstallShardDataApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/InstallShardDataApi.java
@@ -21,7 +21,7 @@ import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import org.apache.solr.client.api.model.InstallShardDataRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 
 /** V2 API definition allowing users to import offline-constructed index into a shard. */
 @Path("/collections/{collName}/shards/{shardName}/install")
@@ -30,7 +30,7 @@ public interface InstallShardDataApi {
   @Operation(
       summary = "Install offline index into an existing shard",
       tags = {"shards"})
-  SolrJerseyResponse installShardData(
+  SubResponseAccumulatingJerseyResponse installShardData(
       @PathParam("collName") String collName,
       @PathParam("shardName") String shardName,
       InstallShardDataRequestBody requestBody)
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/MigrateReplicasApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/MigrateReplicasApi.java
index 566be87b76b..10e918ba9ef 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/MigrateReplicasApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/MigrateReplicasApi.java
@@ -21,7 +21,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
 import org.apache.solr.client.api.model.MigrateReplicasRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 
 /** V2 API definition for migrating replicas from a set of nodes to another set of nodes. */
 @Path("cluster/replicas/migrate")
@@ -30,7 +30,7 @@ public interface MigrateReplicasApi {
   @Operation(
       summary = "Migrate Replicas from a given set of nodes.",
       tags = {"cluster"})
-  SolrJerseyResponse migrateReplicas(
+  SubResponseAccumulatingJerseyResponse migrateReplicas(
       @RequestBody(description = "Contains user provided parameters", required = true)
           MigrateReplicasRequestBody requestBody)
       throws Exception;
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java
index cee4032a196..cb3c577783b 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java
@@ -23,7 +23,7 @@ import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 
 /**
  * V2 API definition for recreating replicas in one node (the source) on another node(s) (the
@@ -35,7 +35,7 @@ public interface ReplaceNodeApi {
   @Operation(
       summary = "'Replace' a specified node by moving all replicas elsewhere",
       tags = {"node"})
-  SolrJerseyResponse replaceNode(
+  SubResponseAccumulatingJerseyResponse replaceNode(
       @Parameter(description = "The name of the node to be replaced.", required = true)
           @PathParam("sourceNodeName")
           String sourceNodeName,
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/CreateCollectionSnapshotResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/CreateCollectionSnapshotResponse.java
index 16944706498..0392e0ed1ce 100644
--- a/solr/api/src/java/org/apache/solr/client/api/model/CreateCollectionSnapshotResponse.java
+++ b/solr/api/src/java/org/apache/solr/client/api/model/CreateCollectionSnapshotResponse.java
@@ -19,7 +19,7 @@ package org.apache.solr.client.api.model;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 
-public class CreateCollectionSnapshotResponse extends AsyncJerseyResponse {
+public class CreateCollectionSnapshotResponse extends SubResponseAccumulatingJerseyResponse {
   @Schema(description = "The name of the collection.")
   public String collection;
 
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/DeleteCollectionSnapshotResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/DeleteCollectionSnapshotResponse.java
index 905b0937f1d..d822e2ae089 100644
--- a/solr/api/src/java/org/apache/solr/client/api/model/DeleteCollectionSnapshotResponse.java
+++ b/solr/api/src/java/org/apache/solr/client/api/model/DeleteCollectionSnapshotResponse.java
@@ -23,7 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 
 /** The Response for {@link org.apache.solr.client.api.endpoint.CollectionSnapshotApis.Delete} */
-public class DeleteCollectionSnapshotResponse extends AsyncJerseyResponse {
+public class DeleteCollectionSnapshotResponse extends SubResponseAccumulatingJerseyResponse {
   @Schema(description = "The name of the collection.")
   @JsonProperty(COLLECTION)
   public String collection;
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
index a71a515c56d..a13bce718d8 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/AddReplicaCmd.java
@@ -25,7 +25,6 @@ import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.TIMEOUT;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 
@@ -76,13 +75,13 @@ public class AddReplicaCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
-    addReplica(state, message, results, null);
+    addReplica(adminCmdContext, message, results, null);
   }
 
   List<ZkNodeProps> addReplica(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       ZkNodeProps message,
       NamedList<Object> results,
       Runnable onComplete)
@@ -103,7 +102,7 @@ public class AddReplicaCmd implements CollApiCmds.CollectionApiCommand {
       collectionName = extCollectionName;
     }
 
-    DocCollection coll = clusterState.getCollection(collectionName);
+    DocCollection coll = adminCmdContext.getClusterState().getCollection(collectionName);
     if (coll == null) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST, "Collection: " + collectionName + " does not exist");
@@ -117,7 +116,6 @@ public class AddReplicaCmd implements CollApiCmds.CollectionApiCommand {
     boolean waitForFinalState = message.getBool(WAIT_FOR_FINAL_STATE, false);
     boolean skipCreateReplicaInClusterState =
         message.getBool(SKIP_CREATE_REPLICA_IN_CLUSTER_STATE, false);
-    final String asyncId = message.getStr(ASYNC);
 
     String node = message.getStr(CoreAdminParams.NODE);
     String createNodeSetStr = message.getStr(CREATE_NODE_SET);
@@ -152,7 +150,7 @@ public class AddReplicaCmd implements CollApiCmds.CollectionApiCommand {
     List<CreateReplica> createReplicas =
         buildReplicaPositions(
                 ccc.getSolrCloudManager(),
-                clusterState,
+                adminCmdContext.getClusterState(),
                 collectionName,
                 message,
                 numReplicas,
@@ -161,14 +159,17 @@ public class AddReplicaCmd implements CollApiCmds.CollectionApiCommand {
             .map(
                 replicaPosition ->
                     assignReplicaDetails(
-                        ccc.getSolrCloudManager(), clusterState, message, replicaPosition))
+                        ccc.getSolrCloudManager(),
+                        adminCmdContext.getClusterState(),
+                        message,
+                        replicaPosition))
             .collect(Collectors.toList());
 
     ShardHandler shardHandler = ccc.newShardHandler();
     ZkStateReader zkStateReader = ccc.getZkStateReader();
 
     final ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     for (CreateReplica createReplica : createReplicas) {
       assert createReplica.coreName != null;
       ModifiableSolrParams params =
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/AdminCmdContext.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/AdminCmdContext.java
new file mode 100644
index 00000000000..48b7c475c74
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/AdminCmdContext.java
@@ -0,0 +1,63 @@
+/*
+ * 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.cloud.api.collections;
+
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.params.CollectionParams;
+
+public class AdminCmdContext {
+  private final CollectionParams.CollectionAction action;
+  private final String asyncId;
+  private ClusterState clusterState;
+
+  public AdminCmdContext(CollectionParams.CollectionAction action) {
+    this(action, null);
+  }
+
+  public AdminCmdContext(CollectionParams.CollectionAction action, String asyncId) {
+    this.action = action;
+    this.asyncId = asyncId;
+  }
+
+  public CollectionParams.CollectionAction getAction() {
+    return action;
+  }
+
+  public String getAsyncId() {
+    return asyncId;
+  }
+
+  public ClusterState getClusterState() {
+    return clusterState;
+  }
+
+  public AdminCmdContext withClusterState(ClusterState clusterState) {
+    this.clusterState = clusterState;
+    return this;
+  }
+
+  public AdminCmdContext subRequestContext(CollectionParams.CollectionAction action) {
+    return subRequestContext(action, asyncId);
+  }
+
+  public AdminCmdContext subRequestContext(
+      CollectionParams.CollectionAction action, String asyncId) {
+    AdminCmdContext nextContext = new AdminCmdContext(action, asyncId);
+    return nextContext.withClusterState(clusterState);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/AliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/AliasCmd.java
index 4815a76b807..ceefa9e889b 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/AliasCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/AliasCmd.java
@@ -25,9 +25,9 @@ import static org.apache.solr.common.params.CommonParams.NAME;
 import java.util.Map;
 import org.apache.solr.cloud.OverseerSolrResponse;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.CollectionProperties;
 import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.handler.admin.CollectionsHandler;
@@ -52,7 +52,7 @@ abstract class AliasCmd implements CollApiCmds.CollectionApiCommand {
    * If the collection already exists then this is not an error.
    */
   static NamedList<Object> createCollectionAndWait(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       String aliasName,
       Map<String, String> aliasMetadata,
       String createCollName,
@@ -83,7 +83,11 @@ abstract class AliasCmd implements CollApiCmds.CollectionApiCommand {
       // CreateCollectionCmd.
       // note: there's doesn't seem to be any point in locking on the collection name, so we don't.
       // We currently should already have a lock on the alias name which should be sufficient.
-      new CreateCollectionCmd(ccc).call(clusterState, createMessage, results);
+      new CreateCollectionCmd(ccc)
+          .call(
+              adminCmdContext.subRequestContext(CollectionParams.CollectionAction.CREATE),
+              createMessage,
+              results);
     } catch (SolrException e) {
       // The collection might already exist, and that's okay -- we can adopt it.
       if (!e.getMessage().contains("collection already exists")) {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/BackupCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/BackupCmd.java
index 151cbeeee1a..60ec1cb7e6c 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/BackupCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/BackupCmd.java
@@ -19,7 +19,6 @@ package org.apache.solr.cloud.api.collections;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_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.CommonParams.NAME;
 
 import java.io.IOException;
@@ -34,7 +33,6 @@ import java.util.Optional;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ShardRequestTracker;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
@@ -71,7 +69,7 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
 
   @SuppressWarnings("unchecked")
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
 
     String extCollectionName = message.getStr(COLLECTION_PROP);
@@ -123,7 +121,13 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
             if (incremental) {
               try {
                 incrementalCopyIndexFiles(
-                    backupUri, collectionName, message, results, backupProperties, backupMgr);
+                    adminCmdContext,
+                    backupUri,
+                    collectionName,
+                    message,
+                    results,
+                    backupProperties,
+                    backupMgr);
               } catch (SolrException e) {
                 log.error(
                     "Error happened during incremental backup for collection: {}",
@@ -135,7 +139,13 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
               }
             } else {
               copyIndexFiles(
-                  backupUri, collectionName, message, results, backupProperties, backupMgr);
+                  adminCmdContext,
+                  backupUri,
+                  collectionName,
+                  message,
+                  results,
+                  backupProperties,
+                  backupMgr);
             }
             break;
           }
@@ -288,6 +298,7 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   private void incrementalCopyIndexFiles(
+      AdminCmdContext adminCmdContext,
       URI backupUri,
       String collectionName,
       ZkNodeProps request,
@@ -296,7 +307,6 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
       BackupManager backupManager)
       throws IOException {
     String backupName = request.getStr(NAME);
-    String asyncId = request.getStr(ASYNC);
     String repoName = request.getStr(CoreAdminParams.BACKUP_REPOSITORY);
     ShardHandler shardHandler = ccc.newShardHandler();
 
@@ -308,7 +318,7 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
 
     Optional<BackupProperties> previousProps = backupManager.tryReadBackupProperties();
     final ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
 
     Collection<Slice> slices =
         ccc.getZkStateReader().getClusterState().getCollection(collectionName).getActiveSlices();
@@ -434,6 +444,7 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   private void copyIndexFiles(
+      AdminCmdContext adminCmdContext,
       URI backupPath,
       String collectionName,
       ZkNodeProps request,
@@ -442,7 +453,6 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
       BackupManager backupManager)
       throws Exception {
     String backupName = request.getStr(NAME);
-    String asyncId = request.getStr(ASYNC);
     String repoName = request.getStr(CoreAdminParams.BACKUP_REPOSITORY);
     ShardHandler shardHandler = ccc.newShardHandler();
 
@@ -484,7 +494,7 @@ public class BackupCmd implements CollApiCmds.CollectionApiCommand {
     }
 
     final ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     Collection<Slice> slices =
         ccc.getZkStateReader().getClusterState().getCollection(collectionName).getActiveSlices();
     for (Slice slice : slices) {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/BalanceReplicasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/BalanceReplicasCmd.java
index 7d84c7c6633..9d1acfb27b5 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/BalanceReplicasCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/BalanceReplicasCmd.java
@@ -17,15 +17,12 @@
 
 package org.apache.solr.cloud.api.collections;
 
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
@@ -41,7 +38,7 @@ public class BalanceReplicasCmd implements CollApiCmds.CollectionApiCommand {
 
   @SuppressWarnings({"unchecked"})
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     Set<String> nodes;
     Object nodesRaw = message.get(CollectionParams.NODES);
@@ -60,7 +57,6 @@ public class BalanceReplicasCmd implements CollApiCmds.CollectionApiCommand {
               + nodesRaw.getClass().getName());
     }
     boolean waitForFinalState = message.getBool(CommonAdminParams.WAIT_FOR_FINAL_STATE, false);
-    String async = message.getStr(ASYNC);
     int timeout = message.getInt("timeout", 10 * 60); // 10 minutes
     boolean parallel = message.getBool("parallel", false);
 
@@ -79,7 +75,7 @@ public class BalanceReplicasCmd implements CollApiCmds.CollectionApiCommand {
 
     boolean migrationSuccessful =
         ReplicaMigrationUtils.migrateReplicas(
-            ccc, replicaMovements, parallel, waitForFinalState, timeout, async, results);
+            ccc, adminCmdContext, replicaMovements, parallel, waitForFinalState, timeout, results);
     if (migrationSuccessful) {
       results.add(
           "success",
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java
index d5bb2d24568..454b5363841 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollApiCmds.java
@@ -65,7 +65,6 @@ import static org.apache.solr.common.params.CollectionParams.CollectionAction.RE
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.REPLACENODE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.RESTORE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.SPLITSHARD;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
 import io.opentelemetry.api.trace.Span;
@@ -81,7 +80,6 @@ import org.apache.solr.cloud.DistributedClusterStateUpdater;
 import org.apache.solr.cloud.Overseer;
 import org.apache.solr.cloud.OverseerNodePrioritizer;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkNodeProps;
@@ -114,7 +112,16 @@ public class CollApiCmds {
    * classes whose names ends in {@code Cmd}.
    */
   protected interface CollectionApiCommand {
-    void call(ClusterState state, ZkNodeProps message, NamedList<Object> results) throws Exception;
+    void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
+        throws Exception;
+
+    default ZkNodeProps cloneZkPropsWithOperation(
+        ZkNodeProps original, CollectionParams.CollectionAction collectionAction) {
+      Map<String, Object> propMap = new HashMap<>();
+      propMap.put(Overseer.QUEUE_OPERATION, collectionAction.toLower());
+      propMap.putAll(original.getProperties());
+      return new ZkNodeProps(propMap);
+    }
   }
 
   /**
@@ -206,7 +213,8 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+    public void call(
+        AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
         throws Exception {
       final Span localSpan;
       final Context localContext;
@@ -217,7 +225,7 @@ public class CollApiCmds {
         String collection =
             Optional.ofNullable(message.getStr(COLLECTION_PROP, message.getStr(NAME)))
                 .orElse("unknown");
-        boolean isAsync = message.containsKey(ASYNC);
+        boolean isAsync = adminCmdContext.getAsyncId() != null;
         localSpan =
             TraceUtils.startCollectionApiCommandSpan(
                 command.getClass().getSimpleName(), collection, isAsync);
@@ -226,7 +234,7 @@ public class CollApiCmds {
 
       try (var scope = localContext.makeCurrent()) {
         assert scope != null; // prevent javac warning about scope being unused
-        command.call(state, message, results);
+        command.call(adminCmdContext, message, results);
       } finally {
         if (localSpan != null) {
           localSpan.end();
@@ -238,7 +246,8 @@ public class CollApiCmds {
   public static class MockOperationCmd implements CollectionApiCommand {
     @Override
     @SuppressForbidden(reason = "Needs currentTimeMillis for mock requests")
-    public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+    public void call(
+        AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
         throws InterruptedException {
       // only for test purposes
       Thread.sleep(message.getInt("sleep", 1));
@@ -260,20 +269,19 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results) {
+    public void call(
+        AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results) {
       ModifiableSolrParams params = new ModifiableSolrParams();
       params.set(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.RELOAD.toString());
 
-      String asyncId = message.getStr(ASYNC);
       CollectionHandlingUtils.collectionCmd(
+          adminCmdContext,
           message,
           params,
           results,
           Replica.State.ACTIVE,
-          asyncId,
           Collections.emptySet(),
-          ccc,
-          clusterState);
+          ccc);
     }
   }
 
@@ -285,7 +293,7 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+    public void call(AdminCmdContext clusterState, ZkNodeProps message, NamedList<Object> results)
         throws Exception {
       CollectionHandlingUtils.checkRequired(
           message,
@@ -326,7 +334,7 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+    public void call(AdminCmdContext clusterState, ZkNodeProps message, NamedList<Object> results)
         throws Exception {
       CollectionHandlingUtils.checkRequired(
           message,
@@ -335,10 +343,7 @@ public class CollApiCmds {
           REPLICA_PROP,
           PROPERTY_PROP,
           PROPERTY_VALUE_PROP);
-      Map<String, Object> propMap = new HashMap<>();
-      propMap.put(Overseer.QUEUE_OPERATION, ADDREPLICAPROP.toLower());
-      propMap.putAll(message.getProperties());
-      ZkNodeProps m = new ZkNodeProps(propMap);
+      ZkNodeProps m = cloneZkPropsWithOperation(message, ADDREPLICAPROP);
       if (ccc.getDistributedClusterStateUpdater().isDistributedStateUpdate()) {
         ccc.getDistributedClusterStateUpdater()
             .doSingleStateUpdate(
@@ -360,14 +365,11 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+    public void call(AdminCmdContext clusterState, ZkNodeProps message, NamedList<Object> results)
         throws Exception {
       CollectionHandlingUtils.checkRequired(
           message, COLLECTION_PROP, SHARD_ID_PROP, REPLICA_PROP, PROPERTY_PROP);
-      Map<String, Object> propMap = new HashMap<>();
-      propMap.put(Overseer.QUEUE_OPERATION, DELETEREPLICAPROP.toLower());
-      propMap.putAll(message.getProperties());
-      ZkNodeProps m = new ZkNodeProps(propMap);
+      ZkNodeProps m = cloneZkPropsWithOperation(message, DELETEREPLICAPROP);
       if (ccc.getDistributedClusterStateUpdater().isDistributedStateUpdate()) {
         ccc.getDistributedClusterStateUpdater()
             .doSingleStateUpdate(
@@ -389,7 +391,7 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+    public void call(AdminCmdContext clusterState, ZkNodeProps message, NamedList<Object> results)
         throws Exception {
       if (StrUtils.isBlank(message.getStr(COLLECTION_PROP))
           || StrUtils.isBlank(message.getStr(PROPERTY_PROP))) {
@@ -401,18 +403,16 @@ public class CollApiCmds {
                 + PROPERTY_PROP
                 + "' parameters are required for the BALANCESHARDUNIQUE operation, no action taken");
       }
-      Map<String, Object> m = new HashMap<>();
-      m.put(Overseer.QUEUE_OPERATION, BALANCESHARDUNIQUE.toLower());
-      m.putAll(message.getProperties());
+      ZkNodeProps m = cloneZkPropsWithOperation(message, BALANCESHARDUNIQUE);
       if (ccc.getDistributedClusterStateUpdater().isDistributedStateUpdate()) {
         ccc.getDistributedClusterStateUpdater()
             .doSingleStateUpdate(
                 DistributedClusterStateUpdater.MutatingCommand.BalanceShardsUnique,
-                new ZkNodeProps(m),
+                m,
                 ccc.getSolrCloudManager(),
                 ccc.getZkStateReader());
       } else {
-        ccc.offerStateUpdate(Utils.toJSON(m));
+        ccc.offerStateUpdate(m);
       }
     }
   }
@@ -425,9 +425,9 @@ public class CollApiCmds {
     }
 
     @Override
-    public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+    public void call(
+        AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
         throws Exception {
-
       final String collectionName = message.getStr(ZkStateReader.COLLECTION_PROP);
       // the rest of the processing is based on writing cluster state properties
       String configName = (String) message.getProperties().get(COLL_CONF);
@@ -446,6 +446,7 @@ public class CollApiCmds {
                   collPath, Utils.toJSON(Map.of(ZkStateReader.CONFIGNAME_PROP, configName)), -1);
         }
       }
+      ZkNodeProps m = cloneZkPropsWithOperation(message, MODIFYCOLLECTION);
 
       if (ccc.getDistributedClusterStateUpdater().isDistributedStateUpdate()) {
         // Apply the state update right away. The wait will still be useful for the change to be
@@ -453,11 +454,11 @@ public class CollApiCmds {
         ccc.getDistributedClusterStateUpdater()
             .doSingleStateUpdate(
                 DistributedClusterStateUpdater.MutatingCommand.CollectionModifyCollection,
-                message,
+                m,
                 ccc.getSolrCloudManager(),
                 ccc.getZkStateReader());
       } else {
-        ccc.offerStateUpdate(message);
+        ccc.offerStateUpdate(m);
       }
 
       try {
@@ -495,12 +496,14 @@ public class CollApiCmds {
                 });
       } catch (TimeoutException | InterruptedException e) {
         SolrZkClient.checkInterrupted(e);
-        log.debug(
-            "modifyCollection(ClusterState={}, ZkNodeProps={}, NamedList={})",
-            clusterState,
-            message,
-            results,
-            e);
+        if (log.isDebugEnabled()) {
+          log.debug(
+              "modifyCollection(ClusterState={}, ZkNodeProps={}, NamedList={})",
+              ccc.getZkStateReader().getClusterState(),
+              message,
+              results,
+              e);
+        }
         throw new SolrException(
             SolrException.ErrorCode.SERVER_ERROR, "Failed to modify collection", e);
       }
@@ -508,7 +511,10 @@ public class CollApiCmds {
       // if switching to/from read-only mode or configName is not null reload the collection
       if (message.keySet().contains(ZkStateReader.READ_ONLY) || configName != null) {
         new ReloadCollectionCmd(ccc)
-            .call(clusterState, new ZkNodeProps(NAME, collectionName), results);
+            .call(
+                adminCmdContext.subRequestContext(RELOAD, null),
+                new ZkNodeProps(NAME, collectionName),
+                results);
       }
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionApiLockFactory.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionApiLockFactory.java
index 56ffb7b6439..2c5342c008d 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionApiLockFactory.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionApiLockFactory.java
@@ -17,6 +17,7 @@
 
 package org.apache.solr.cloud.api.collections;
 
+import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.solr.cloud.DistributedCollectionLockFactory;
@@ -24,12 +25,15 @@ import org.apache.solr.cloud.DistributedLock;
 import org.apache.solr.cloud.DistributedMultiLock;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CollectionParams;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * This class implements a higher level locking abstraction for the Collection API using lower level
  * read and write locks.
  */
 public class CollectionApiLockFactory {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   private final DistributedCollectionLockFactory lockFactory;
 
@@ -54,7 +58,8 @@ public class CollectionApiLockFactory {
    *     prevent other threads from locking.
    */
   DistributedMultiLock createCollectionApiLock(
-      CollectionParams.LockLevel lockLevel, String collName, String shardId, String replicaName) {
+      AdminCmdContext adminCmdContext, String collName, String shardId, String replicaName) {
+    CollectionParams.LockLevel lockLevel = adminCmdContext.getAction().lockLevel;
     if (lockLevel == CollectionParams.LockLevel.NONE) {
       return new DistributedMultiLock(List.of());
     }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionCommandContext.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionCommandContext.java
index f167d4dc6b4..20c3e935e29 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionCommandContext.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionCommandContext.java
@@ -55,9 +55,12 @@ public interface CollectionCommandContext {
 
   CoreContainer getCoreContainer();
 
-  default ShardRequestTracker asyncRequestTracker(String asyncId) {
+  default ShardRequestTracker asyncRequestTracker(AdminCmdContext adminCmdContext) {
     return new ShardRequestTracker(
-        asyncId, getAdminPath(), getZkStateReader(), newShardHandler().getShardHandlerFactory());
+        adminCmdContext.getAsyncId(),
+        getAdminPath(),
+        getZkStateReader(),
+        newShardHandler().getShardHandlerFactory());
   }
 
   /** admin path passed to Overseer is a constant, including in tests. */
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java
index c44a52e2e7d..74232b11ddd 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java
@@ -307,13 +307,17 @@ public class CollectionHandlingUtils {
   }
 
   static void cleanupCollection(
-      String collectionName, NamedList<Object> results, CollectionCommandContext ccc)
+      AdminCmdContext adminCmdContext,
+      String collectionName,
+      NamedList<Object> results,
+      CollectionCommandContext ccc)
       throws Exception {
     log.error("Cleaning up collection [{}].", collectionName);
-    Map<String, Object> props =
-        Map.of(Overseer.QUEUE_OPERATION, DELETE.toLower(), NAME, collectionName);
     new DeleteCollectionCmd(ccc)
-        .call(ccc.getZkStateReader().getClusterState(), new ZkNodeProps(props), results);
+        .call(
+            adminCmdContext.subRequestContext(DELETE),
+            new ZkNodeProps(NAME, collectionName),
+            results);
   }
 
   static Map<String, Replica> waitToSeeReplicasInState(
@@ -372,17 +376,6 @@ public class CollectionHandlingUtils {
     new DeleteBackupCmd(ccc).keepNumberOfBackup(repository, backupPath, maxNumBackup, results);
   }
 
-  static List<ZkNodeProps> addReplica(
-      ClusterState clusterState,
-      ZkNodeProps message,
-      NamedList<Object> results,
-      Runnable onComplete,
-      CollectionCommandContext ccc)
-      throws Exception {
-
-    return new AddReplicaCmd(ccc).addReplica(clusterState, message, results, onComplete);
-  }
-
   static void validateConfigOrThrowSolrException(
       ConfigSetService configSetService, String configName) throws IOException {
     boolean isValid = configSetService.checkConfigExists(configName);
@@ -399,24 +392,26 @@ public class CollectionHandlingUtils {
    * @return List of replicas which is not live for receiving the request
    */
   static List<Replica> collectionCmd(
+      AdminCmdContext adminCmdContext,
       ZkNodeProps message,
       ModifiableSolrParams params,
       NamedList<Object> results,
       Replica.State stateMatcher,
-      String asyncId,
       Set<String> okayExceptions,
-      CollectionCommandContext ccc,
-      ClusterState clusterState) {
-    log.info("Executing Collection Cmd={}, asyncId={}", params, asyncId);
+      CollectionCommandContext ccc) {
+    if (log.isInfoEnabled()) {
+      log.info("Executing Collection Cmd={}, asyncId={}", params, adminCmdContext.getAsyncId());
+    }
     String collectionName = message.getStr(NAME);
     ShardHandler shardHandler = ccc.newShardHandler();
-    DocCollection coll = clusterState.getCollection(collectionName);
+    DocCollection coll = ccc.getZkStateReader().getClusterState().getCollection(collectionName);
     List<Replica> notLivesReplicas = new ArrayList<>();
     final CollectionHandlingUtils.ShardRequestTracker shardRequestTracker =
-        asyncRequestTracker(asyncId, ccc);
+        asyncRequestTracker(adminCmdContext, ccc);
     for (Slice slice : coll.getSlices()) {
       notLivesReplicas.addAll(
-          shardRequestTracker.sliceCmd(clusterState, params, stateMatcher, slice, shardHandler));
+          shardRequestTracker.sliceCmd(
+              ccc.getZkStateReader().getClusterState(), params, stateMatcher, slice, shardHandler));
     }
 
     shardRequestTracker.processResponses(results, shardHandler, false, null, okayExceptions);
@@ -574,11 +569,17 @@ public class CollectionHandlingUtils {
     } while (true);
   }
 
-  public static ShardRequestTracker syncRequestTracker(CollectionCommandContext ccc) {
-    return asyncRequestTracker(null, ccc);
+  public static ShardRequestTracker syncRequestTracker(
+      AdminCmdContext adminCmdContext, CollectionCommandContext ccc) {
+    return requestTracker(null, ccc);
   }
 
   public static ShardRequestTracker asyncRequestTracker(
+      AdminCmdContext adminCmdContext, CollectionCommandContext ccc) {
+    return requestTracker(adminCmdContext.getAsyncId(), ccc);
+  }
+
+  protected static ShardRequestTracker requestTracker(
       String asyncId, CollectionCommandContext ccc) {
     return new ShardRequestTracker(
         asyncId,
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateAliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateAliasCmd.java
index f48a984aad1..53de754285f 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateAliasCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateAliasCmd.java
@@ -50,7 +50,7 @@ public class CreateAliasCmd extends AliasCmd {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     final String aliasName = message.getStr(CommonParams.NAME);
     ZkStateReader zkStateReader = ccc.getZkStateReader();
@@ -63,7 +63,7 @@ public class CreateAliasCmd extends AliasCmd {
     if (!anyRoutingParams(message)) {
       callCreatePlainAlias(message, aliasName, zkStateReader);
     } else {
-      callCreateRoutedAlias(message, aliasName, zkStateReader, state);
+      callCreateRoutedAlias(adminCmdContext, message, aliasName, zkStateReader);
     }
 
     // Sleep a bit to allow ZooKeeper state propagation.
@@ -112,7 +112,10 @@ public class CreateAliasCmd extends AliasCmd {
   }
 
   private void callCreateRoutedAlias(
-      ZkNodeProps message, String aliasName, ZkStateReader zkStateReader, ClusterState state)
+      AdminCmdContext adminCmdContext,
+      ZkNodeProps message,
+      String aliasName,
+      ZkStateReader zkStateReader)
       throws Exception {
     // Validate we got a basic minimum
     if (!message.getProperties().keySet().containsAll(RoutedAlias.MINIMAL_REQUIRED_PARAMS)) {
@@ -150,7 +153,11 @@ public class CreateAliasCmd extends AliasCmd {
       // Create the first collection. Prior validation ensures that this is not a standard alias
       collectionListStr = routedAlias.computeInitialCollectionName();
       ensureAliasCollection(
-          aliasName, zkStateReader, state, routedAlias.getAliasMetadata(), collectionListStr);
+          adminCmdContext,
+          aliasName,
+          zkStateReader,
+          routedAlias.getAliasMetadata(),
+          collectionListStr);
     } else {
       List<String> collectionList = aliases.resolveAliases(aliasName);
       collectionListStr = String.join(",", collectionList);
@@ -163,14 +170,15 @@ public class CreateAliasCmd extends AliasCmd {
   }
 
   private void ensureAliasCollection(
+      AdminCmdContext adminCmdContext,
       String aliasName,
       ZkStateReader zkStateReader,
-      ClusterState state,
       Map<String, String> aliasProperties,
       String initialCollectionName)
       throws Exception {
     // Create the collection
-    createCollectionAndWait(state, aliasName, aliasProperties, initialCollectionName, ccc);
+    createCollectionAndWait(
+        adminCmdContext, aliasName, aliasProperties, initialCollectionName, ccc);
     validateAllCollectionsExistAndNoDuplicates(
         Collections.singletonList(initialCollectionName), zkStateReader);
   }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java
index acd82c44203..f9aab00769d 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateCollectionCmd.java
@@ -20,6 +20,8 @@ package org.apache.solr.cloud.api.collections;
 import static org.apache.solr.common.params.CollectionAdminParams.ALIAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETE;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.apache.solr.common.params.CommonParams.NAME;
@@ -101,11 +103,12 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     if (ccc.getZkStateReader().aliasesManager != null) { // not a mock ZkStateReader
       ccc.getZkStateReader().aliasesManager.update();
     }
+    ClusterState clusterState = adminCmdContext.getClusterState();
     final Aliases aliases = ccc.getZkStateReader().getAliases();
     final String collectionName = message.getStr(NAME);
     final boolean waitForFinalState = message.getBool(WAIT_FOR_FINAL_STATE, false);
@@ -151,9 +154,6 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
     final String collectionPath = DocCollection.getCollectionPath(collectionName);
 
     try {
-
-      final String async = message.getStr(ASYNC);
-
       ZkStateReader zkStateReader = ccc.getZkStateReader();
       message.getProperties().put(COLL_CONF, configName);
 
@@ -174,6 +174,8 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
           collectionParams,
           ccc.getCoreContainer().getConfigSetService());
 
+      ZkNodeProps m = cloneZkPropsWithOperation(message, CREATE);
+
       // Note that in code below there are two main execution paths: Overseer based cluster state
       // updates and distributed cluster state updates (look for isDistributedStateUpdate()
       // conditions).
@@ -196,8 +198,7 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
 
         // TODO: Consider doing this for all collections, not just the PRS collections.
         ZkWriteCommand command =
-            new ClusterStateMutator(ccc.getSolrCloudManager())
-                .createCollection(clusterState, message);
+            new ClusterStateMutator(ccc.getSolrCloudManager()).createCollection(clusterState, m);
         byte[] data = Utils.toJSON(Collections.singletonMap(collectionName, command.collection));
         ccc.getZkStateReader().getZkClient().create(collectionPath, data, CreateMode.PERSISTENT);
         clusterState = clusterState.copyWith(collectionName, command.collection);
@@ -210,11 +211,11 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
           ccc.getDistributedClusterStateUpdater()
               .doSingleStateUpdate(
                   DistributedClusterStateUpdater.MutatingCommand.ClusterCreateCollection,
-                  message,
+                  m,
                   ccc.getSolrCloudManager(),
                   ccc.getZkStateReader());
         } else {
-          ccc.offerStateUpdate(Utils.toJSON(message));
+          ccc.offerStateUpdate(m);
         }
 
         // wait for a while until we see the collection
@@ -245,7 +246,11 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
                 numReplicas);
       } catch (Assign.AssignmentException e) {
         ZkNodeProps deleteMessage = new ZkNodeProps("name", collectionName);
-        new DeleteCollectionCmd(ccc).call(clusterState, deleteMessage, results);
+        new DeleteCollectionCmd(ccc)
+            .call(
+                adminCmdContext.subRequestContext(DELETE).withClusterState(clusterState),
+                deleteMessage,
+                results);
         // unwrap the exception
         throw new SolrException(ErrorCode.BAD_REQUEST, e.getMessage(), e.getCause());
       }
@@ -256,7 +261,7 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
       }
 
       final ShardRequestTracker shardRequestTracker =
-          CollectionHandlingUtils.asyncRequestTracker(async, ccc);
+          CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
       if (log.isDebugEnabled()) {
         log.debug(
             formatString(
@@ -339,7 +344,7 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
           if (ccc.getDistributedClusterStateUpdater().isDistributedStateUpdate()) {
             scr.record(DistributedClusterStateUpdater.MutatingCommand.SliceAddReplica, props);
           } else {
-            ccc.offerStateUpdate(Utils.toJSON(props));
+            ccc.offerStateUpdate(props);
           }
         }
 
@@ -355,8 +360,8 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
         params.set(CoreAdminParams.NEW_COLLECTION, "true");
         params.set(CoreAdminParams.REPLICA_TYPE, replicaPosition.type.name());
 
-        if (async != null) {
-          String coreAdminAsyncId = async + Math.abs(System.nanoTime());
+        if (adminCmdContext.getAsyncId() != null) {
+          String coreAdminAsyncId = adminCmdContext.getAsyncId() + Math.abs(System.nanoTime());
           params.add(ASYNC, coreAdminAsyncId);
         }
         CollectionHandlingUtils.addPropertyParams(message, params);
@@ -438,7 +443,8 @@ public class CreateCollectionCmd implements CollApiCmds.CollectionApiCommand {
         // Let's cleanup as we hit an exception
         // We shouldn't be passing 'results' here for the cleanup as the response would then contain
         // 'success' element, which may be interpreted by the user as a positive ack
-        CollectionHandlingUtils.cleanupCollection(collectionName, new NamedList<>(), ccc);
+        CollectionHandlingUtils.cleanupCollection(
+            adminCmdContext, collectionName, new NamedList<>(), ccc);
         log.info("Cleaned up artifacts for failed create collection for [{}]", collectionName);
         throw new SolrException(
             ErrorCode.BAD_REQUEST,
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateShardCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateShardCmd.java
index b76250cfd8d..7c0e1633603 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateShardCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateShardCmd.java
@@ -19,7 +19,7 @@ package org.apache.solr.cloud.api.collections;
 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.CollectionParams.CollectionAction.CREATESHARD;
 
 import java.lang.invoke.MethodHandles;
 import java.util.Map;
@@ -29,6 +29,7 @@ import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.ReplicaCount;
 import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CommonAdminParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
@@ -45,7 +46,7 @@ public class CreateShardCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String extCollectionName = message.getStr(COLLECTION_PROP);
     String sliceName = message.getStr(SHARD_ID_PROP);
@@ -64,6 +65,7 @@ public class CreateShardCmd implements CollApiCmds.CollectionApiCommand {
     } else {
       collectionName = extCollectionName;
     }
+    ClusterState clusterState = adminCmdContext.getClusterState();
     DocCollection collection = clusterState.getCollection(collectionName);
 
     ReplicaCount numReplicas = ReplicaCount.fromMessage(message, collection, 1);
@@ -75,6 +77,8 @@ public class CreateShardCmd implements CollApiCmds.CollectionApiCommand {
               + "), there must be at least one leader-eligible replica");
     }
 
+    ZkNodeProps m = cloneZkPropsWithOperation(message, CREATESHARD);
+
     if (ccc.getDistributedClusterStateUpdater().isDistributedStateUpdate()) {
       // The message has been crafted by CollectionsHandler.CollectionOperation.CREATESHARD_OP and
       // defines the QUEUE_OPERATION to be CollectionParams.CollectionAction.CREATESHARD. Likely a
@@ -83,20 +87,20 @@ public class CreateShardCmd implements CollApiCmds.CollectionApiCommand {
       ccc.getDistributedClusterStateUpdater()
           .doSingleStateUpdate(
               DistributedClusterStateUpdater.MutatingCommand.CollectionCreateShard,
-              message,
+              m,
               ccc.getSolrCloudManager(),
               ccc.getZkStateReader());
     } else {
       // message contains extCollectionName that might be an alias. Unclear (to me) how this works
       // in that case.
-      ccc.offerStateUpdate(message);
+      ccc.offerStateUpdate(m);
     }
 
     // wait for a while until we see the shard and update the local view of the cluster state
     clusterState =
         CollectionHandlingUtils.waitForNewShard(collectionName, sliceName, ccc.getZkStateReader());
 
-    String async = message.getStr(ASYNC);
+    String async = adminCmdContext.getAsyncId();
     Map<String, Object> addReplicasProps =
         Utils.makeMap(
             COLLECTION_PROP,
@@ -110,14 +114,13 @@ public class CreateShardCmd implements CollApiCmds.CollectionApiCommand {
     numReplicas.writeProps(addReplicasProps);
 
     CollectionHandlingUtils.addPropertyParams(message, addReplicasProps);
-    if (async != null) {
-      addReplicasProps.put(ASYNC, async);
-    }
     final NamedList<Object> addResult = new NamedList<>();
     try {
       new AddReplicaCmd(ccc)
           .addReplica(
-              clusterState,
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.ADDREPLICA, async)
+                  .withClusterState(clusterState),
               new ZkNodeProps(addReplicasProps),
               addResult,
               () -> {
@@ -147,9 +150,13 @@ public class CreateShardCmd implements CollApiCmds.CollectionApiCommand {
               });
     } catch (Assign.AssignmentException e) {
       // clean up the slice that we created
-      ZkNodeProps deleteShard =
-          new ZkNodeProps(COLLECTION_PROP, collectionName, SHARD_ID_PROP, sliceName, ASYNC, async);
-      new DeleteShardCmd(ccc).call(clusterState, deleteShard, results);
+      new DeleteShardCmd(ccc)
+          .call(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.DELETESHARD, async)
+                  .withClusterState(clusterState),
+              new ZkNodeProps(COLLECTION_PROP, collectionName, SHARD_ID_PROP, sliceName),
+              results);
       throw e;
     }
 
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java
index 3ce2ae38b4a..97e24fc8279 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CreateSnapshotCmd.java
@@ -19,7 +19,6 @@ package org.apache.solr.cloud.api.collections;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_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.CommonParams.NAME;
 
 import java.lang.invoke.MethodHandles;
@@ -33,7 +32,6 @@ import java.util.Set;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ShardRequestTracker;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
@@ -62,7 +60,7 @@ public class CreateSnapshotCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String extCollectionName = message.getStr(COLLECTION_PROP);
     boolean followAliases = message.getBool(FOLLOW_ALIASES, false);
@@ -75,7 +73,6 @@ public class CreateSnapshotCmd implements CollApiCmds.CollectionApiCommand {
     }
 
     String commitName = message.getStr(CoreAdminParams.COMMIT_NAME);
-    String asyncId = message.getStr(ASYNC);
     SolrZkClient zkClient = ccc.getZkStateReader().getZkClient();
     Date creationDate = new Date();
 
@@ -101,7 +98,7 @@ public class CreateSnapshotCmd implements CollApiCmds.CollectionApiCommand {
     ShardHandler shardHandler = ccc.newShardHandler();
 
     final ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     for (Slice slice :
         ccc.getZkStateReader().getClusterState().getCollection(collectionName).getSlices()) {
       for (Replica replica : slice.getReplicas()) {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteAliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteAliasCmd.java
index 2c99dc36ef7..1d90b4e8797 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteAliasCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteAliasCmd.java
@@ -19,7 +19,6 @@ package org.apache.solr.cloud.api.collections;
 
 import static org.apache.solr.common.params.CommonParams.NAME;
 
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.util.NamedList;
@@ -32,7 +31,7 @@ public class DeleteAliasCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String aliasName = message.getStr(NAME);
 
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
index aa6c8f9ef5b..dfec0e3bb0e 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
@@ -36,7 +36,6 @@ import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 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.CoreAdminParams;
 import org.apache.solr.common.util.NamedList;
@@ -71,7 +70,7 @@ public class DeleteBackupCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String backupLocation = message.getStr(CoreAdminParams.BACKUP_LOCATION);
     String backupName = message.getStr(NAME);
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java
index 4298ee3f6d3..9c292863ece 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteCollectionCmd.java
@@ -19,7 +19,6 @@ package org.apache.solr.cloud.api.collections;
 
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETE;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
 import java.lang.invoke.MethodHandles;
@@ -36,7 +35,6 @@ import org.apache.solr.cloud.Overseer;
 import org.apache.solr.common.NonExistentCoreException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.Aliases;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.SolrZkClient;
@@ -64,7 +62,7 @@ public class DeleteCollectionCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     Object o = message.get(MaintainRoutedAliasCmd.INVOKED_BY_ROUTED_ALIAS);
     if (o != null) {
@@ -91,7 +89,7 @@ public class DeleteCollectionCmd implements CollApiCmds.CollectionApiCommand {
     }
 
     // verify the placement modifications caused by the deletion are allowed
-    DocCollection coll = state.getCollectionOrNull(collection);
+    DocCollection coll = zkStateReader.getClusterState().getCollectionOrNull(collection);
     if (coll != null) {
       Assign.AssignStrategy assignStrategy = Assign.createAssignStrategy(ccc.getCoreContainer());
       assignStrategy.verifyDeleteCollection(ccc.getSolrCloudManager(), coll);
@@ -119,13 +117,11 @@ public class DeleteCollectionCmd implements CollApiCmds.CollectionApiCommand {
       params.set(CoreAdminParams.DELETE_INSTANCE_DIR, true);
       params.set(CoreAdminParams.DELETE_DATA_DIR, true);
 
-      String asyncId = message.getStr(ASYNC);
-
       ZkNodeProps internalMsg = message.plus(NAME, collection);
 
       List<Replica> failedReplicas =
           CollectionHandlingUtils.collectionCmd(
-              internalMsg, params, results, null, asyncId, okayExceptions, ccc, state);
+              adminCmdContext, internalMsg, params, results, null, okayExceptions, ccc);
       for (Replica failedReplica : failedReplicas) {
         boolean isSharedFS =
             failedReplica.getBool(ZkStateReader.SHARED_STORAGE_PROP, false)
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java
index b93980ff7da..656eb374709 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteNodeCmd.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.cloud.api.collections;
 
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.solr.common.cloud.ClusterState;
@@ -36,12 +34,14 @@ public class DeleteNodeCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     CollectionHandlingUtils.checkRequired(message, "node");
     String node = message.getStr("node");
-    List<Replica> sourceReplicas = ReplicaMigrationUtils.getReplicasOfNode(node, state);
-    List<String> singleReplicas = verifyReplicaAvailability(sourceReplicas, state);
+    List<Replica> sourceReplicas =
+        ReplicaMigrationUtils.getReplicasOfNode(node, adminCmdContext.getClusterState());
+    List<String> singleReplicas =
+        verifyReplicaAvailability(sourceReplicas, adminCmdContext.getClusterState());
     if (!singleReplicas.isEmpty()) {
       results.add(
           "failure",
@@ -50,8 +50,7 @@ public class DeleteNodeCmd implements CollApiCmds.CollectionApiCommand {
               + ": "
               + singleReplicas);
     } else {
-      ReplicaMigrationUtils.cleanupReplicas(
-          results, state, sourceReplicas, ccc, message.getStr(ASYNC));
+      ReplicaMigrationUtils.cleanupReplicas(results, adminCmdContext, sourceReplicas, ccc);
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java
index 138dec2ef55..5246401db6e 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteReplicaCmd.java
@@ -21,7 +21,6 @@ import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_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 java.io.IOException;
 import java.lang.invoke.MethodHandles;
@@ -36,7 +35,6 @@ import java.util.stream.Collectors;
 import org.apache.solr.cloud.api.collections.CollApiCmds.CollectionApiCommand;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ShardRequestTracker;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
@@ -61,13 +59,13 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
-    deleteReplica(clusterState, message, results, null);
+    deleteReplica(adminCmdContext, message, results, null);
   }
 
   void deleteReplica(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       ZkNodeProps message,
       NamedList<Object> results,
       Runnable onComplete)
@@ -79,7 +77,7 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
 
     // If a count is specified the strategy needs be different
     if (message.getStr(COUNT_PROP) != null) {
-      deleteReplicaBasedOnCount(clusterState, message, results, onComplete, parallel);
+      deleteReplicaBasedOnCount(adminCmdContext, message, results, onComplete, parallel);
       return;
     }
 
@@ -97,14 +95,15 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
       collectionName = extCollectionName;
     }
 
-    DocCollection coll = clusterState.getCollection(collectionName);
+    DocCollection coll = adminCmdContext.getClusterState().getCollection(collectionName);
     Slice slice = coll.getSlice(shard);
     if (slice == null) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
           "Invalid shard name : " + shard + " in collection : " + collectionName);
     }
-    deleteCore(coll, shard, replicaName, message, results, onComplete, parallel, true);
+    deleteCore(
+        adminCmdContext, coll, shard, replicaName, message, results, onComplete, parallel, true);
   }
 
   /**
@@ -112,7 +111,7 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
    * deletes given num replicas across all shards for the given collection.
    */
   void deleteReplicaBasedOnCount(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       ZkNodeProps message,
       NamedList<Object> results,
       Runnable onComplete,
@@ -122,7 +121,7 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
     int count = Integer.parseInt(message.getStr(COUNT_PROP));
     String collectionName = message.getStr(COLLECTION_PROP);
     String shard = message.getStr(SHARD_ID_PROP);
-    DocCollection coll = clusterState.getCollection(collectionName);
+    DocCollection coll = adminCmdContext.getClusterState().getCollection(collectionName);
     Slice slice = null;
     // Validate if shard is passed.
     if (shard != null) {
@@ -172,7 +171,8 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
       for (String replica : replicas) {
         log.debug("Deleting replica {}  for shard {} based on count {}", replica, shardId, count);
         // don't verify with the placement plugin - we already did it
-        deleteCore(coll, shardId, replica, message, results, onComplete, parallel, false);
+        deleteCore(
+            adminCmdContext, coll, shardId, replica, message, results, onComplete, parallel, false);
       }
       results.add("shard_id", shardId);
       results.add("replicas_deleted", replicas);
@@ -242,6 +242,7 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
   }
 
   void deleteCore(
+      AdminCmdContext adminCmdContext,
       DocCollection coll,
       String shardId,
       String replicaName,
@@ -295,7 +296,6 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
 
     ShardHandler shardHandler = ccc.newShardHandler();
     String core = replica.getStr(ZkStateReader.CORE_NAME_PROP);
-    String asyncId = message.getStr(ASYNC);
 
     ModifiableSolrParams params = new ModifiableSolrParams();
     params.add(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.UNLOAD.toString());
@@ -311,7 +311,7 @@ public class DeleteReplicaCmd implements CollectionApiCommand {
     boolean isLive =
         ccc.getZkStateReader().getClusterState().getLiveNodes().contains(replica.getNodeName());
     final ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     if (isLive) {
       shardRequestTracker.sendShardRequest(replica.getNodeName(), params, shardHandler);
     }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteShardCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteShardCmd.java
index d32b80d4b39..1234925c94c 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteShardCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteShardCmd.java
@@ -20,8 +20,8 @@ import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.NODE_NAME_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.CollectionParams.CollectionAction.DELETEREPLICA;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETESHARD;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
@@ -35,7 +35,6 @@ import org.apache.solr.cloud.DistributedClusterStateUpdater;
 import org.apache.solr.cloud.Overseer;
 import org.apache.solr.cloud.overseer.OverseerAction;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.SolrZkClient;
@@ -58,7 +57,7 @@ public class DeleteShardCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String extCollectionName = message.getStr(ZkStateReader.COLLECTION_PROP);
     String sliceId = message.getStr(ZkStateReader.SHARD_ID_PROP);
@@ -73,7 +72,7 @@ public class DeleteShardCmd implements CollApiCmds.CollectionApiCommand {
     }
 
     log.info("Delete shard invoked");
-    Slice slice = clusterState.getCollection(collectionName).getSlice(sliceId);
+    Slice slice = adminCmdContext.getClusterState().getCollection(collectionName).getSlice(sliceId);
     if (slice == null)
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
@@ -125,14 +124,11 @@ public class DeleteShardCmd implements CollApiCmds.CollectionApiCommand {
       }
     }
 
-    String asyncId = message.getStr(ASYNC);
-
     try {
       List<ZkNodeProps> replicas = getReplicasForSlice(collectionName, slice);
       CountDownLatch cleanupLatch = new CountDownLatch(replicas.size());
       for (ZkNodeProps r : replicas) {
-        final ZkNodeProps replica =
-            r.plus(message.getProperties()).plus("parallel", "true").plus(ASYNC, asyncId);
+        final ZkNodeProps replica = r.plus(message.getProperties()).plus("parallel", "true");
         if (log.isInfoEnabled()) {
           log.info(
               "Deleting replica for collection={} shard={} on node={}",
@@ -144,7 +140,7 @@ public class DeleteShardCmd implements CollApiCmds.CollectionApiCommand {
         try {
           new DeleteReplicaCmd(ccc)
               .deleteReplica(
-                  clusterState,
+                  adminCmdContext.subRequestContext(DELETEREPLICA),
                   replica,
                   deleteResult,
                   () -> {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteSnapshotCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteSnapshotCmd.java
index b3970210cb6..ffb93c48fc2 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteSnapshotCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteSnapshotCmd.java
@@ -19,7 +19,6 @@ package org.apache.solr.cloud.api.collections;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_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.CommonParams.NAME;
 
 import java.lang.invoke.MethodHandles;
@@ -31,7 +30,6 @@ import java.util.Set;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ShardRequestTracker;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
 import org.apache.solr.common.cloud.Slice;
@@ -60,7 +58,7 @@ public class DeleteSnapshotCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String extCollectionName = message.getStr(COLLECTION_PROP);
     boolean followAliases = message.getBool(FOLLOW_ALIASES, false);
@@ -71,7 +69,6 @@ public class DeleteSnapshotCmd implements CollApiCmds.CollectionApiCommand {
       collectionName = extCollectionName;
     }
     String commitName = message.getStr(CoreAdminParams.COMMIT_NAME);
-    String asyncId = message.getStr(ASYNC);
     NamedList<Object> shardRequestResults = new NamedList<>();
     ShardHandler shardHandler = ccc.newShardHandler();
     SolrZkClient zkClient = ccc.getZkStateReader().getZkClient();
@@ -101,7 +98,7 @@ public class DeleteSnapshotCmd implements CollApiCmds.CollectionApiCommand {
     }
 
     final ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     log.info(
         "Existing cores with snapshot for collection={} are {}", collectionName, existingCores);
     for (Slice slice :
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java
index 465cc4d6273..4cc2c2ca1bf 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java
@@ -22,7 +22,6 @@ import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.logF
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
 import java.io.IOException;
@@ -46,7 +45,6 @@ import org.apache.solr.cloud.ZkDistributedConfigSetLockFactory;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkNodeProps;
-import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ConfigSetParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.util.ExecutorUtil;
@@ -252,7 +250,7 @@ public class DistributedCollectionConfigSetCommandRunner {
    * </ul>
    */
   public OverseerSolrResponse runCollectionCommand(
-      ZkNodeProps message, CollectionParams.CollectionAction action, long timeoutMs) {
+      AdminCmdContext adminCmdContext, ZkNodeProps message, long timeoutMs) {
     // We refuse new tasks, but will wait for already submitted ones (i.e. those that made it
     // through this method earlier). See stopAndWaitForPendingTasksToComplete() below
     if (shuttingDown) {
@@ -261,35 +259,35 @@ public class DistributedCollectionConfigSetCommandRunner {
           "Solr is shutting down, no more Collection API tasks may be executed");
     }
 
-    final String asyncId = message.getStr(ASYNC);
-
     if (log.isInfoEnabled()) {
       log.info(
-          "Running Collection API locally for " + action.name() + " asyncId=" + asyncId); // nowarn
+          "Running Collection API locally for {} asyncId={}",
+          adminCmdContext.getAction().name(),
+          adminCmdContext.getAsyncId());
     }
 
     // Following the call below returning true, we must eventually cancel or complete the task.
     // Happens either in the CollectionCommandRunner below or in the catch when the runner would not
     // execute.
-    if (!asyncTaskTracker.createNewAsyncJobTracker(asyncId)) {
+    if (!asyncTaskTracker.createNewAsyncJobTracker(adminCmdContext.getAsyncId())) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
-          "Task with the same requestid already exists. (" + asyncId + ")");
+          "Task with the same requestid already exists. (" + adminCmdContext.getAsyncId() + ")");
     }
 
-    CollectionCommandRunner commandRunner = new CollectionCommandRunner(message, action, asyncId);
+    CollectionCommandRunner commandRunner = new CollectionCommandRunner(adminCmdContext, message);
     final Future<OverseerSolrResponse> taskFuture;
     try {
       taskFuture = commandsExecutor.submit(commandRunner);
     } catch (RejectedExecutionException ree) {
       // The command will not run, need to cancel the async ID so it can be reused on a subsequent
       // attempt by the client
-      asyncTaskTracker.cancelAsyncId(asyncId);
+      asyncTaskTracker.cancelAsyncId(adminCmdContext.getAsyncId());
       throw new SolrException(
           SolrException.ErrorCode.SERVICE_UNAVAILABLE, "Too many executing commands", ree);
     }
 
-    if (asyncId == null) {
+    if (adminCmdContext.getAsyncId() == null) {
       // Non async calls wait for a while in case the command completes. If they time out, there's
       // no way to track the job progress (improvement suggestion: decorrelate having a task ID from
       // the fact of waiting for the job to complete)
@@ -297,18 +295,21 @@ public class DistributedCollectionConfigSetCommandRunner {
         return taskFuture.get(timeoutMs, TimeUnit.MILLISECONDS);
       } catch (TimeoutException te) {
         throw new SolrException(
-            SolrException.ErrorCode.SERVER_ERROR, action + " timed out after " + timeoutMs + "ms");
+            SolrException.ErrorCode.SERVER_ERROR,
+            adminCmdContext.getAction() + " timed out after " + timeoutMs + "ms");
       } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
-        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, action + " interrupted", e);
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR, adminCmdContext.getAction() + " interrupted", e);
       } catch (Exception e) {
-        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, action + " failed", e);
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR, adminCmdContext.getAction() + " failed", e);
       }
     } else {
       // Async calls do not wait for the command to finish but get instead back the async id (that
       // they just sent...)
       NamedList<Object> resp = new NamedList<>();
-      resp.add(CoreAdminParams.REQUESTID, asyncId);
+      resp.add(CoreAdminParams.REQUESTID, adminCmdContext.getAsyncId());
       return new OverseerSolrResponse(resp);
     }
   }
@@ -357,15 +358,12 @@ public class DistributedCollectionConfigSetCommandRunner {
    * similar to the one provided by Overseer based Collection API execution.
    */
   private class CollectionCommandRunner implements Callable<OverseerSolrResponse> {
+    private final AdminCmdContext adminCmdContext;
     private final ZkNodeProps message;
-    private final CollectionParams.CollectionAction action;
-    private final String asyncId;
 
-    private CollectionCommandRunner(
-        ZkNodeProps message, CollectionParams.CollectionAction action, String asyncId) {
+    private CollectionCommandRunner(AdminCmdContext adminCmdContext, ZkNodeProps message) {
+      this.adminCmdContext = adminCmdContext;
       this.message = message;
-      this.action = action;
-      this.asyncId = asyncId;
     }
 
     /**
@@ -398,36 +396,44 @@ public class DistributedCollectionConfigSetCommandRunner {
             new CollectionApiLockFactory(
                     new ZkDistributedCollectionLockFactory(
                         ccc.getZkStateReader().getZkClient(), ZK_COLLECTION_LOCKS))
-                .createCollectionApiLock(action.lockLevel, collName, shardId, replicaName);
+                .createCollectionApiLock(adminCmdContext, collName, shardId, replicaName);
 
         try {
-          log.debug(
-              "CollectionCommandRunner about to acquire lock for action {} lock level {}. {}/{}/{}",
-              action,
-              action.lockLevel,
-              collName,
-              shardId,
-              replicaName);
+          if (log.isDebugEnabled()) {
+            log.debug(
+                "CollectionCommandRunner about to acquire lock for action {} lock level {}. {}/{}/{}",
+                adminCmdContext.getAction().name(),
+                adminCmdContext.getAction().lockLevel,
+                collName,
+                shardId,
+                replicaName);
+          }
 
           // Block this thread until all required locks are acquired.
           lock.waitUntilAcquired();
 
           // Got the lock so moving from submitted to running if we run for an async task (if
           // asyncId is null the asyncTaskTracker calls do nothing).
-          asyncTaskTracker.setTaskRunning(asyncId);
+          asyncTaskTracker.setTaskRunning(adminCmdContext.getAsyncId());
 
-          log.debug(
-              "DistributedCollectionConfigSetCommandRunner.runCollectionCommand. Lock acquired. Calling: {}, {}",
-              action,
-              message);
+          if (log.isDebugEnabled()) {
+            log.debug(
+                "DistributedCollectionConfigSetCommandRunner.runCollectionCommand. Lock acquired. Calling: {}, {}",
+                adminCmdContext.getAction(),
+                message);
+          }
 
-          CollApiCmds.CollectionApiCommand command = commandMapper.getActionCommand(action);
+          CollApiCmds.CollectionApiCommand command =
+              commandMapper.getActionCommand(adminCmdContext.getAction());
           if (command != null) {
-            command.call(ccc.getSolrCloudManager().getClusterState(), message, results);
+            command.call(
+                adminCmdContext.withClusterState(ccc.getSolrCloudManager().getClusterState()),
+                message,
+                results);
           } else {
-            asyncTaskTracker.cancelAsyncId(asyncId);
+            asyncTaskTracker.cancelAsyncId(adminCmdContext.getAsyncId());
             // Seeing this is a bug, not bad user data
-            String message = "Bug: Unknown operation " + action;
+            String message = "Bug: Unknown operation " + adminCmdContext.getAction();
             log.error(message);
             throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, message);
           }
@@ -439,21 +445,25 @@ public class DistributedCollectionConfigSetCommandRunner {
             // hierarchy do no harm, and there shouldn't be too many of those.
             lock.release();
           } catch (SolrException se) {
-            log.error(
-                "Error when releasing collection locks for operation " + action, se); // nowarn
+            if (log.isErrorEnabled()) {
+              log.error(
+                  "Error when releasing collection locks for operation {}",
+                  adminCmdContext.getAction(),
+                  se);
+            }
           }
         }
       } catch (Exception e) {
         if (e instanceof InterruptedException) {
           Thread.currentThread().interrupt();
         }
-        logFailedOperation(action, e, collName);
-        addExceptionToNamedList(action, e, results);
+        logFailedOperation(adminCmdContext.getAction(), e, collName);
+        addExceptionToNamedList(adminCmdContext.getAction(), e, results);
       }
 
       OverseerSolrResponse res = new OverseerSolrResponse(results);
       // Following call marks success or failure depending on the contents of res
-      asyncTaskTracker.setTaskCompleted(asyncId, res);
+      asyncTaskTracker.setTaskCompleted(adminCmdContext.getAsyncId(), res);
       return res;
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/InstallShardDataCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/InstallShardDataCmd.java
index 18ce5aa4071..ca654a150ae 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/InstallShardDataCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/InstallShardDataCmd.java
@@ -58,12 +58,13 @@ public class InstallShardDataCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  @SuppressWarnings("unchecked")
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     final RemoteMessage typedMessage =
         new ObjectMapper().convertValue(message.getProperties(), RemoteMessage.class);
     final CollectionHandlingUtils.ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(typedMessage.asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     final ClusterState clusterState = ccc.getZkStateReader().getClusterState();
     typedMessage.validate();
 
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/MaintainRoutedAliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/MaintainRoutedAliasCmd.java
index 3320ea0b5b7..e6c316c2aa6 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/MaintainRoutedAliasCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/MaintainRoutedAliasCmd.java
@@ -25,10 +25,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.client.solrj.SolrResponse;
-import org.apache.solr.cloud.Overseer;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.Aliases;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.CollectionProperties;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
@@ -57,14 +55,13 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
    */
   static void remoteInvoke(CollectionsHandler collHandler, String aliasName, String targetCol)
       throws Exception {
-    final CollectionParams.CollectionAction maintainroutedalias =
-        CollectionParams.CollectionAction.MAINTAINROUTEDALIAS;
-    Map<String, Object> msg = new HashMap<>();
-    msg.put(Overseer.QUEUE_OPERATION, maintainroutedalias.toLower());
-    msg.put(CollectionParams.NAME, aliasName);
-    msg.put(MaintainRoutedAliasCmd.ROUTED_ALIAS_TARGET_COL, targetCol);
     final SolrResponse rsp =
-        collHandler.submitCollectionApiCommand(new ZkNodeProps(msg), maintainroutedalias);
+        collHandler.submitCollectionApiCommand(
+            new AdminCmdContext(CollectionParams.CollectionAction.MAINTAINROUTEDALIAS),
+            new ZkNodeProps(
+                Map.of(
+                    CollectionParams.NAME, aliasName,
+                    MaintainRoutedAliasCmd.ROUTED_ALIAS_TARGET_COL, targetCol)));
     if (rsp.getException() != null) {
       throw rsp.getException();
     }
@@ -109,7 +106,7 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     // ---- PARSE PRIMARY MESSAGE PARAMS
     // important that we use NAME for the alias as that is what the Overseer will get a lock on
@@ -146,7 +143,7 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
                     () -> {
                       try {
                         deleteTargetCollection(
-                            clusterState, results, aliasName, aliasesManager, action);
+                            adminCmdContext, results, aliasName, aliasesManager, action);
                       } catch (Exception e) {
                         log.warn(
                             "Deletion of {} by {} {} failed (this might be ok if two clients were",
@@ -161,7 +158,7 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
         case ENSURE_EXISTS:
           if (!exists) {
             addTargetCollection(
-                clusterState, results, aliasName, aliasesManager, aliasMetadata, action);
+                adminCmdContext, results, aliasName, aliasesManager, aliasMetadata, action);
           } else {
             // check that the collection is properly integrated into the alias (see
             // TimeRoutedAliasUpdateProcessorTest.java:141). Presently we need to ensure inclusion
@@ -189,7 +186,7 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
   }
 
   public void addTargetCollection(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       NamedList<Object> results,
       String aliasName,
       ZkStateReader.AliasesManager aliasesManager,
@@ -198,7 +195,7 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
       throws Exception {
     NamedList<Object> createResults =
         createCollectionAndWait(
-            clusterState, aliasName, aliasMetadata, action.targetCollection, ccc);
+            adminCmdContext, aliasName, aliasMetadata, action.targetCollection, ccc);
     if (createResults != null) {
       results.add("create", createResults);
     }
@@ -206,7 +203,7 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
   }
 
   public void deleteTargetCollection(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       NamedList<Object> results,
       String aliasName,
       ZkStateReader.AliasesManager aliasesManager,
@@ -219,6 +216,10 @@ public class MaintainRoutedAliasCmd extends AliasCmd {
             () -> removeCollectionFromAlias(aliasName, aliasesManager, action.targetCollection));
     delProps.put(NAME, action.targetCollection);
     ZkNodeProps messageDelete = new ZkNodeProps(delProps);
-    new DeleteCollectionCmd(ccc).call(clusterState, messageDelete, results);
+    new DeleteCollectionCmd(ccc)
+        .call(
+            adminCmdContext.subRequestContext(CollectionParams.CollectionAction.DELETE),
+            messageDelete,
+            results);
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateCmd.java
index b09f806535a..ba56c8ab9dd 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateCmd.java
@@ -23,7 +23,6 @@ import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETE;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
 import java.lang.invoke.MethodHandles;
@@ -38,7 +37,6 @@ import org.apache.solr.cloud.Overseer;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ShardRequestTracker;
 import org.apache.solr.cloud.overseer.OverseerAction;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.CompositeIdRouter;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.DocRouter;
@@ -65,7 +63,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String extSourceCollectionName = message.getStr("collection");
     String splitKey = message.getStr("split.key");
@@ -89,13 +87,15 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
       targetCollectionName = extTargetCollectionName;
     }
 
-    DocCollection sourceCollection = clusterState.getCollection(sourceCollectionName);
+    DocCollection sourceCollection =
+        adminCmdContext.getClusterState().getCollection(sourceCollectionName);
     if (sourceCollection == null) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
           "Unknown source collection: " + sourceCollectionName);
     }
-    DocCollection targetCollection = clusterState.getCollection(targetCollectionName);
+    DocCollection targetCollection =
+        adminCmdContext.getClusterState().getCollection(targetCollectionName);
     if (targetCollection == null) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
@@ -136,9 +136,6 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
               + splitKey);
     }
 
-    String asyncId = null;
-    if (message.containsKey(ASYNC) && message.get(ASYNC) != null) asyncId = message.getStr(ASYNC);
-
     for (Slice sourceSlice : sourceSlices) {
       for (Slice targetSlice : targetSlices) {
         log.info(
@@ -147,7 +144,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
             targetSlice,
             splitKey);
         migrateKey(
-            clusterState,
+            adminCmdContext,
             sourceCollection,
             sourceSlice,
             targetCollection,
@@ -155,14 +152,13 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
             splitKey,
             timeout,
             results,
-            asyncId,
             message);
       }
     }
   }
 
   private void migrateKey(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       DocCollection sourceCollection,
       Slice sourceSlice,
       DocCollection targetCollection,
@@ -170,21 +166,22 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
       String splitKey,
       int timeout,
       NamedList<Object> results,
-      String asyncId,
       ZkNodeProps message)
       throws Exception {
     String tempSourceCollectionName =
         "split_" + sourceSlice.getName() + "_temp_" + targetSlice.getName();
     ZkStateReader zkStateReader = ccc.getZkStateReader();
-    if (clusterState.hasCollection(tempSourceCollectionName)) {
+    if (adminCmdContext.getClusterState().hasCollection(tempSourceCollectionName)) {
       log.info("Deleting temporary collection: {}", tempSourceCollectionName);
-      Map<String, Object> props =
-          Map.of(Overseer.QUEUE_OPERATION, DELETE.toLower(), NAME, tempSourceCollectionName);
-
       try {
         new DeleteCollectionCmd(ccc)
-            .call(zkStateReader.getClusterState(), new ZkNodeProps(props), results);
-        clusterState = zkStateReader.getClusterState();
+            .call(
+                adminCmdContext
+                    .subRequestContext(DELETE)
+                    .withClusterState(zkStateReader.getClusterState()),
+                new ZkNodeProps(Map.of(NAME, tempSourceCollectionName)),
+                results);
+        adminCmdContext.withClusterState(zkStateReader.getClusterState());
       } catch (Exception e) {
         log.warn(
             "Unable to clean up existing temporary collection: {}", tempSourceCollectionName, e);
@@ -234,7 +231,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
 
     {
       final ShardRequestTracker shardRequestTracker =
-          CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+          CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
       shardRequestTracker.sendShardRequest(targetLeader.getNodeName(), params, shardHandler);
 
       shardRequestTracker.processResponses(
@@ -312,17 +309,26 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
             configName,
             CollectionHandlingUtils.CREATE_NODE_SET,
             sourceLeader.getNodeName());
-    if (asyncId != null) {
-      String internalAsyncId = asyncId + Math.abs(System.nanoTime());
-      props.put(ASYNC, internalAsyncId);
+    String internalAsyncId = null;
+    if (adminCmdContext.getAsyncId() != null) {
+      internalAsyncId = adminCmdContext.getAsyncId() + Math.abs(System.nanoTime());
     }
 
     log.info("Creating temporary collection: {}", props);
-    new CreateCollectionCmd(ccc).call(clusterState, new ZkNodeProps(props), results);
+    new CreateCollectionCmd(ccc)
+        .call(
+            adminCmdContext.subRequestContext(CREATE, internalAsyncId),
+            new ZkNodeProps(props),
+            results);
     // refresh cluster state
-    clusterState = zkStateReader.getClusterState();
+    adminCmdContext.withClusterState(zkStateReader.getClusterState());
     Slice tempSourceSlice =
-        clusterState.getCollection(tempSourceCollectionName).getSlices().iterator().next();
+        adminCmdContext
+            .getClusterState()
+            .getCollection(tempSourceCollectionName)
+            .getSlices()
+            .iterator()
+            .next();
     Replica tempSourceLeader =
         zkStateReader.getLeaderRetry(tempSourceCollectionName, tempSourceSlice.getName(), 120000);
 
@@ -349,7 +355,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
     cmd.setOnlyIfLeader(true);
     {
       final ShardRequestTracker syncRequestTracker =
-          CollectionHandlingUtils.syncRequestTracker(ccc);
+          CollectionHandlingUtils.syncRequestTracker(adminCmdContext, ccc);
       // we don't want this to happen asynchronously
       syncRequestTracker.sendShardRequest(
           tempSourceLeader.getNodeName(), new ModifiableSolrParams(cmd.getParams()), shardHandler);
@@ -374,7 +380,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
 
     {
       final ShardRequestTracker shardRequestTracker =
-          CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+          CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
       shardRequestTracker.sendShardRequest(tempNodeName, params, shardHandler);
       shardRequestTracker.processResponses(
           results, shardHandler, true, "MIGRATE failed to invoke SPLIT core admin command");
@@ -392,7 +398,6 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
             tempSourceSlice.getName(),
             Replica.Type.defaultType());
     props = new HashMap<>();
-    props.put(Overseer.QUEUE_OPERATION, ADDREPLICA.toLower());
     props.put(COLLECTION_PROP, tempSourceCollectionName);
     props.put(SHARD_ID_PROP, tempSourceSlice.getName());
     props.put("node", targetLeader.getNodeName());
@@ -404,14 +409,13 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
       }
     }
     // add async param
-    if (asyncId != null) {
-      props.put(ASYNC, asyncId);
-    }
-    new AddReplicaCmd(ccc).addReplica(clusterState, new ZkNodeProps(props), results, null);
+    new AddReplicaCmd(ccc)
+        .addReplica(
+            adminCmdContext.subRequestContext(ADDREPLICA), new ZkNodeProps(props), results, null);
 
     {
       final ShardRequestTracker syncRequestTracker =
-          CollectionHandlingUtils.syncRequestTracker(ccc);
+          CollectionHandlingUtils.syncRequestTracker(adminCmdContext, ccc);
       syncRequestTracker.processResponses(
           results,
           shardHandler,
@@ -442,7 +446,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
 
     {
       final ShardRequestTracker shardRequestTracker =
-          CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+          CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
       shardRequestTracker.sendShardRequest(tempSourceLeader.getNodeName(), params, shardHandler);
 
       shardRequestTracker.processResponses(
@@ -462,7 +466,7 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
 
     {
       final ShardRequestTracker shardRequestTracker =
-          CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+          CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
 
       shardRequestTracker.sendShardRequest(targetLeader.getNodeName(), params, shardHandler);
       String msg =
@@ -482,16 +486,20 @@ public class MigrateCmd implements CollApiCmds.CollectionApiCommand {
 
     {
       final ShardRequestTracker shardRequestTracker =
-          CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+          CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
       shardRequestTracker.sendShardRequest(targetLeader.getNodeName(), params, shardHandler);
       shardRequestTracker.processResponses(
           results, shardHandler, true, "MIGRATE failed to request node to apply buffered updates");
     }
     try {
       log.info("Deleting temporary collection: {}", tempSourceCollectionName);
-      props = Map.of(Overseer.QUEUE_OPERATION, DELETE.toLower(), NAME, tempSourceCollectionName);
       new DeleteCollectionCmd(ccc)
-          .call(zkStateReader.getClusterState(), new ZkNodeProps(props), results);
+          .call(
+              adminCmdContext
+                  .subRequestContext(DELETE)
+                  .withClusterState(zkStateReader.getClusterState()),
+              new ZkNodeProps(Map.of(NAME, tempSourceCollectionName)),
+              results);
     } catch (Exception e) {
       log.error(
           "Unable to delete temporary collection: {}. Please remove it manually",
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateReplicasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateReplicasCmd.java
index 3912e096cb6..e4381ad888e 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateReplicasCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/MigrateReplicasCmd.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.cloud.api.collections;
 
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -48,7 +46,7 @@ public class MigrateReplicasCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     ZkStateReader zkStateReader = ccc.getZkStateReader();
     Set<String> sourceNodes = getNodesFromParam(message, CollectionParams.SOURCE_NODES);
@@ -58,7 +56,6 @@ public class MigrateReplicasCmd implements CollApiCmds.CollectionApiCommand {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST, "sourceNodes is a required param");
     }
-    String async = message.getStr(ASYNC);
     int timeout = message.getInt("timeout", 10 * 60); // 10 minutes
     boolean parallel = message.getBool("parallel", false);
     ClusterState clusterState = zkStateReader.getClusterState();
@@ -121,7 +118,7 @@ public class MigrateReplicasCmd implements CollApiCmds.CollectionApiCommand {
 
     boolean migrationSuccessful =
         ReplicaMigrationUtils.migrateReplicas(
-            ccc, replicaMovements, parallel, waitForFinalState, timeout, async, results);
+            ccc, adminCmdContext, replicaMovements, parallel, waitForFinalState, timeout, results);
     if (migrationSuccessful) {
       results.add(
           "success",
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/MoveReplicaCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/MoveReplicaCmd.java
index 9bcae03e812..89a5f32c930 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/MoveReplicaCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/MoveReplicaCmd.java
@@ -22,7 +22,6 @@ import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_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.CommonAdminParams.IN_PLACE_MOVE;
 import static org.apache.solr.common.params.CommonAdminParams.TIMEOUT;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
@@ -37,7 +36,6 @@ import java.util.concurrent.TimeoutException;
 import org.apache.solr.cloud.ActiveReplicaWatcher;
 import org.apache.solr.common.SolrCloseableLatch;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
@@ -60,13 +58,14 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
-    moveReplica(ccc.getZkStateReader().getClusterState(), message, results);
+    moveReplica(adminCmdContext, message, results);
   }
 
   private void moveReplica(
-      ClusterState clusterState, ZkNodeProps message, NamedList<Object> results) throws Exception {
+      AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
+      throws Exception {
     if (log.isDebugEnabled()) {
       log.debug("moveReplica() : {}", Utils.toJSONString(message));
     }
@@ -77,8 +76,6 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
     boolean inPlaceMove = message.getBool(IN_PLACE_MOVE, true);
     int timeout = message.getInt(TIMEOUT, 10 * 60); // 10 minutes
 
-    String async = message.getStr(ASYNC);
-
     boolean followAliases = message.getBool(FOLLOW_ALIASES, false);
     String collection;
     if (followAliases) {
@@ -88,15 +85,18 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
       collection = extCollection;
     }
 
-    DocCollection coll = clusterState.getCollection(collection);
+    DocCollection coll = adminCmdContext.getClusterState().getCollection(collection);
     if (coll == null) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST, "Collection: " + collection + " does not exist");
     }
-    if (!clusterState.getLiveNodes().contains(targetNode)) {
+    if (!adminCmdContext.getClusterState().getLiveNodes().contains(targetNode)) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
-          "Target node: " + targetNode + " not in live nodes: " + clusterState.getLiveNodes());
+          "Target node: "
+              + targetNode
+              + " not in live nodes: "
+              + adminCmdContext.getClusterState().getLiveNodes());
     }
     Replica replica = null;
     if (message.containsKey(REPLICA_PROP)) {
@@ -156,41 +156,31 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
     if (isSharedFS && inPlaceMove) {
       log.debug("-- moveSharedFsReplica");
       moveSharedFsReplica(
-          clusterState,
+          adminCmdContext,
           results,
           dataDir.toString(),
           targetNode,
-          async,
           replica,
           timeout,
           waitForFinalState);
     } else {
       log.debug("-- moveNormalReplica (inPlaceMove={}, isSharedFS={}", inPlaceMove, isSharedFS);
       moveNormalReplica(
-          clusterState,
-          results,
-          targetNode,
-          async,
-          coll,
-          replica,
-          slice,
-          timeout,
-          waitForFinalState);
+          adminCmdContext, results, targetNode, coll, replica, slice, timeout, waitForFinalState);
     }
   }
 
   private void moveSharedFsReplica(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       NamedList<Object> results,
       String dataDir,
       String targetNode,
-      String async,
       Replica replica,
       int timeout,
       boolean waitForFinalState)
       throws Exception {
     String skipCreateReplicaInClusterState = "true";
-    if (clusterState.getLiveNodes().contains(replica.getNodeName())) {
+    if (adminCmdContext.getClusterState().getLiveNodes().contains(replica.getNodeName())) {
       skipCreateReplicaInClusterState = "false";
       ZkNodeProps removeReplicasProps =
           new ZkNodeProps(
@@ -199,11 +189,14 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
               REPLICA_PROP, replica.getName());
       removeReplicasProps.getProperties().put(CoreAdminParams.DELETE_DATA_DIR, false);
       removeReplicasProps.getProperties().put(CoreAdminParams.DELETE_INDEX, false);
-      if (async != null) removeReplicasProps.getProperties().put(ASYNC, async);
       NamedList<Object> deleteResult = new NamedList<>();
       try {
         new DeleteReplicaCmd(ccc)
-            .deleteReplica(clusterState, removeReplicasProps, deleteResult, null);
+            .deleteReplica(
+                adminCmdContext.subRequestContext(CollectionParams.CollectionAction.DELETEREPLICA),
+                removeReplicasProps,
+                deleteResult,
+                null);
       } catch (SolrException e) {
         // assume this failed completely so there's nothing to roll back
         deleteResult.add("failure", e.toString());
@@ -259,11 +252,16 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
             ZkStateReader.REPLICA_TYPE,
             replica.getType().name());
 
-    if (async != null) addReplicasProps.getProperties().put(ASYNC, async);
     NamedList<Object> addResult = new NamedList<>();
     try {
       new AddReplicaCmd(ccc)
-          .addReplica(ccc.getZkStateReader().getClusterState(), addReplicasProps, addResult, null);
+          .addReplica(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.ADDREPLICA)
+                  .withClusterState(ccc.getZkStateReader().getClusterState()),
+              addReplicasProps,
+              addResult,
+              null);
     } catch (Exception e) {
       // fatal error - try rolling back
       String errorString =
@@ -279,7 +277,13 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
       addReplicasProps = addReplicasProps.plus(CoreAdminParams.NODE, replica.getNodeName());
       NamedList<Object> rollback = new NamedList<>();
       new AddReplicaCmd(ccc)
-          .addReplica(ccc.getZkStateReader().getClusterState(), addReplicasProps, rollback, null);
+          .addReplica(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.ADDREPLICA)
+                  .withClusterState(ccc.getZkStateReader().getClusterState()),
+              addReplicasProps,
+              rollback,
+              null);
       if (rollback.get("failure") != null) {
         throw new SolrException(
             SolrException.ErrorCode.SERVER_ERROR,
@@ -307,7 +311,13 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
       NamedList<Object> rollback = new NamedList<>();
       try {
         new AddReplicaCmd(ccc)
-            .addReplica(ccc.getZkStateReader().getClusterState(), addReplicasProps, rollback, null);
+            .addReplica(
+                adminCmdContext
+                    .subRequestContext(CollectionParams.CollectionAction.ADDREPLICA)
+                    .withClusterState(ccc.getZkStateReader().getClusterState()),
+                addReplicasProps,
+                rollback,
+                null);
       } catch (Exception e) {
         throw new SolrException(
             SolrException.ErrorCode.SERVER_ERROR,
@@ -337,10 +347,9 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   private void moveNormalReplica(
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       NamedList<Object> results,
       String targetNode,
-      String async,
       DocCollection coll,
       Replica replica,
       Slice slice,
@@ -366,12 +375,17 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
             ZkStateReader.REPLICA_TYPE,
             replica.getType().name());
 
-    if (async != null) addReplicasProps.getProperties().put(ASYNC, async);
     NamedList<Object> addResult = new NamedList<>();
     SolrCloseableLatch countDownLatch = new SolrCloseableLatch(1, ccc.getCloseableToLatchOn());
     ActiveReplicaWatcher watcher = null;
     ZkNodeProps props =
-        new AddReplicaCmd(ccc).addReplica(clusterState, addReplicasProps, addResult, null).get(0);
+        new AddReplicaCmd(ccc)
+            .addReplica(
+                adminCmdContext.subRequestContext(CollectionParams.CollectionAction.ADDREPLICA),
+                addReplicasProps,
+                addResult,
+                null)
+            .get(0);
     log.debug("props {}", props);
     if (replica.equals(slice.getLeader()) || waitForFinalState) {
       watcher =
@@ -427,11 +441,14 @@ public class MoveReplicaCmd implements CollApiCmds.CollectionApiCommand {
             COLLECTION_PROP, coll.getName(),
             SHARD_ID_PROP, slice.getName(),
             REPLICA_PROP, replica.getName());
-    if (async != null) removeReplicasProps.getProperties().put(ASYNC, async);
     NamedList<Object> deleteResult = new NamedList<>();
     try {
       new DeleteReplicaCmd(ccc)
-          .deleteReplica(clusterState, removeReplicasProps, deleteResult, null);
+          .deleteReplica(
+              adminCmdContext.subRequestContext(CollectionParams.CollectionAction.DELETEREPLICA),
+              removeReplicasProps,
+              deleteResult,
+              null);
     } catch (SolrException e) {
       deleteResult.add("failure", e.toString());
     }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerCollectionMessageHandler.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerCollectionMessageHandler.java
index df933b597fe..793195c40f3 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerCollectionMessageHandler.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerCollectionMessageHandler.java
@@ -21,6 +21,7 @@ import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.logF
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
 import java.io.IOException;
@@ -127,7 +128,9 @@ public class OverseerCollectionMessageHandler implements OverseerMessageHandler,
       CollectionAction action = getCollectionAction(operation);
       CollApiCmds.CollectionApiCommand command = commandMapper.getActionCommand(action);
       if (command != null) {
-        command.call(cloudManager.getClusterState(), message, results);
+        AdminCmdContext adminCmdContext = new AdminCmdContext(action, message.getStr(ASYNC));
+        adminCmdContext.withClusterState(cloudManager.getClusterState());
+        command.call(adminCmdContext, message, results);
       } else {
         throw new SolrException(ErrorCode.BAD_REQUEST, "Unknown operation:" + operation);
       }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerRoleCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerRoleCmd.java
index fc400ffb26a..32f10bc1c6f 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerRoleCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerRoleCmd.java
@@ -25,7 +25,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.cloud.OverseerNodePrioritizer;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
@@ -55,7 +54,7 @@ public class OverseerRoleCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     if (ccc.isDistributedCollectionAPI()) {
       // No Overseer (not accessible from Collection API command execution in any case) so this
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java
index 74b2fd57bf9..031808b5b05 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java
@@ -24,7 +24,6 @@ import java.util.List;
 import java.util.Map;
 import org.apache.solr.cloud.OverseerTaskProcessor;
 import org.apache.solr.cloud.Stats;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.util.NamedList;
@@ -157,7 +156,7 @@ public class OverseerStatusCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     // If Collection API execution is distributed, we're not running on the Overseer node so can't
     // return any Overseer stats.
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReindexCollectionCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReindexCollectionCmd.java
index 8909841c936..e72cfbe04a4 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReindexCollectionCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReindexCollectionCmd.java
@@ -169,7 +169,7 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
 
     log.debug("*** called: {}", message);
@@ -188,6 +188,7 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
     } else {
       collection = extCollection;
     }
+    ClusterState clusterState = ccc.getZkStateReader().getClusterState();
     if (!clusterState.hasCollection(collection)) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST, "Source collection name must exist");
@@ -293,13 +294,13 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
       }
       if (clusterState.hasCollection(chkCollection)) {
         // delete the checkpoint collection
-        cmd =
-            new ZkNodeProps(
-                Overseer.QUEUE_OPERATION,
-                CollectionParams.CollectionAction.DELETE.toLower(),
-                CommonParams.NAME,
-                chkCollection);
-        new DeleteCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+        new DeleteCollectionCmd(ccc)
+            .call(
+                adminCmdContext
+                    .subRequestContext(CollectionParams.CollectionAction.DELETE, null)
+                    .withClusterState(clusterState),
+                new ZkNodeProps(CommonParams.NAME, chkCollection),
+                cmdResults);
         CollectionHandlingUtils.checkResults(
             "deleting old checkpoint collection " + chkCollection, cmdResults, true);
       }
@@ -310,7 +311,6 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
       }
 
       Map<String, Object> propMap = new HashMap<>();
-      propMap.put(Overseer.QUEUE_OPERATION, CollectionParams.CollectionAction.CREATE.toLower());
       propMap.put(CommonParams.NAME, targetCollection);
       propMap.put(ZkStateReader.NUM_SHARDS_PROP, numShards);
       propMap.put(CollectionAdminParams.COLL_CONF, configName);
@@ -338,7 +338,13 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
       // create the target collection
       cmd = new ZkNodeProps(propMap);
       cmdResults = new NamedList<>();
-      new CreateCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+      new CreateCollectionCmd(ccc)
+          .call(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.CREATE, null)
+                  .withClusterState(ccc.getSolrCloudManager().getClusterState()),
+              cmd,
+              cmdResults);
       createdTarget = true;
       CollectionHandlingUtils.checkResults(
           "creating target collection " + targetCollection, cmdResults, true);
@@ -346,14 +352,19 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
       // create the checkpoint collection - use RF=1 and 1 shard
       cmd =
           new ZkNodeProps(
-              Overseer.QUEUE_OPERATION, CollectionParams.CollectionAction.CREATE.toLower(),
               CommonParams.NAME, chkCollection,
               ZkStateReader.NUM_SHARDS_PROP, "1",
               ZkStateReader.REPLICATION_FACTOR, "1",
               CollectionAdminParams.COLL_CONF, "_default",
               CommonAdminParams.WAIT_FOR_FINAL_STATE, "true");
       cmdResults = new NamedList<>();
-      new CreateCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+      new CreateCollectionCmd(ccc)
+          .call(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.CREATE, null)
+                  .withClusterState(ccc.getSolrCloudManager().getClusterState()),
+              cmd,
+              cmdResults);
       CollectionHandlingUtils.checkResults(
           "creating checkpoint collection " + chkCollection, cmdResults, true);
       // wait for a while until we see both collections
@@ -366,8 +377,6 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
             SolrException.ErrorCode.SERVER_ERROR, "Could not fully create temporary collection(s)");
       }
 
-      clusterState = ccc.getSolrCloudManager().getClusterState();
-
       if (maybeAbort(collection)) {
         aborted = true;
         return;
@@ -480,7 +489,13 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
         log.debug("- setting up alias from {} to {}", extCollection, targetCollection);
         cmd = new ZkNodeProps(CommonParams.NAME, extCollection, "collections", targetCollection);
         cmdResults = new NamedList<>();
-        new CreateAliasCmd(ccc).call(clusterState, cmd, cmdResults);
+        new CreateAliasCmd(ccc)
+            .call(
+                adminCmdContext
+                    .subRequestContext(CollectionParams.CollectionAction.CREATEALIAS, null)
+                    .withClusterState(ccc.getSolrCloudManager().getClusterState()),
+                cmd,
+                cmdResults);
         CollectionHandlingUtils.checkResults(
             "setting up alias " + extCollection + " -> " + targetCollection, cmdResults, true);
         reindexingState.put("alias", extCollection + " -> " + targetCollection);
@@ -498,30 +513,29 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
       }
       // 6. delete the checkpoint collection
       log.debug("- deleting {}", chkCollection);
-      cmd =
-          new ZkNodeProps(
-              Overseer.QUEUE_OPERATION,
-              CollectionParams.CollectionAction.DELETE.toLower(),
-              CommonParams.NAME,
-              chkCollection);
       cmdResults = new NamedList<>();
-      new DeleteCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+      new DeleteCollectionCmd(ccc)
+          .call(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.DELETE, null)
+                  .withClusterState(ccc.getSolrCloudManager().getClusterState()),
+              new ZkNodeProps(CommonParams.NAME, chkCollection),
+              cmdResults);
       CollectionHandlingUtils.checkResults(
           "deleting checkpoint collection " + chkCollection, cmdResults, true);
 
       // 7. optionally delete the source collection
       if (removeSource) {
         log.debug("- deleting source collection");
-        cmd =
-            new ZkNodeProps(
-                Overseer.QUEUE_OPERATION,
-                CollectionParams.CollectionAction.DELETE.toLower(),
-                CommonParams.NAME,
-                collection,
-                FOLLOW_ALIASES,
-                "false");
+        cmd = new ZkNodeProps(CommonParams.NAME, collection, FOLLOW_ALIASES, "false");
         cmdResults = new NamedList<>();
-        new DeleteCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+        new DeleteCollectionCmd(ccc)
+            .call(
+                adminCmdContext
+                    .subRequestContext(CollectionParams.CollectionAction.DELETE, null)
+                    .withClusterState(ccc.getSolrCloudManager().getClusterState()),
+                cmd,
+                cmdResults);
         CollectionHandlingUtils.checkResults(
             "deleting source collection " + collection, cmdResults, true);
       } else {
@@ -575,6 +589,7 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
     } finally {
       if (aborted) {
         cleanup(
+            adminCmdContext,
             collection,
             targetCollection,
             chkCollection,
@@ -869,6 +884,7 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   private void cleanup(
+      AdminCmdContext adminCmdContext,
       String collection,
       String targetCollection,
       String chkCollection,
@@ -891,30 +907,29 @@ public class ReindexCollectionCmd implements CollApiCmds.CollectionApiCommand {
         && clusterState.hasCollection(targetCollection)) {
       log.debug(" -- removing {}", targetCollection);
       ZkNodeProps cmd =
-          new ZkNodeProps(
-              Overseer.QUEUE_OPERATION,
-              CollectionParams.CollectionAction.DELETE.toLower(),
-              CommonParams.NAME,
-              targetCollection,
-              FOLLOW_ALIASES,
-              "false");
-      new DeleteCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+          new ZkNodeProps(CommonParams.NAME, targetCollection, FOLLOW_ALIASES, "false");
+      new DeleteCollectionCmd(ccc)
+          .call(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.DELETE, null)
+                  .withClusterState(clusterState),
+              cmd,
+              cmdResults);
       CollectionHandlingUtils.checkResults(
           "CLEANUP: deleting target collection " + targetCollection, cmdResults, false);
     }
     // remove chk collection
     if (clusterState.hasCollection(chkCollection)) {
       log.debug(" -- removing {}", chkCollection);
-      ZkNodeProps cmd =
-          new ZkNodeProps(
-              Overseer.QUEUE_OPERATION,
-              CollectionParams.CollectionAction.DELETE.toLower(),
-              CommonParams.NAME,
-              chkCollection,
-              FOLLOW_ALIASES,
-              "false");
+      ZkNodeProps cmd = new ZkNodeProps(CommonParams.NAME, chkCollection, FOLLOW_ALIASES, "false");
       cmdResults = new NamedList<>();
-      new DeleteCollectionCmd(ccc).call(clusterState, cmd, cmdResults);
+      new DeleteCollectionCmd(ccc)
+          .call(
+              adminCmdContext
+                  .subRequestContext(CollectionParams.CollectionAction.DELETE, null)
+                  .withClusterState(clusterState),
+              cmd,
+              cmdResults);
       CollectionHandlingUtils.checkResults(
           "CLEANUP: deleting checkpoint collection " + chkCollection, cmdResults, false);
     }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/RenameCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/RenameCmd.java
index 8677ecf72ef..be0c3cfa32b 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/RenameCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/RenameCmd.java
@@ -22,7 +22,6 @@ import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES
 import java.lang.invoke.MethodHandles;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.Aliases;
-import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionAdminParams;
 import org.apache.solr.common.params.CoreAdminParams;
@@ -41,7 +40,7 @@ public class RenameCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String extCollectionName = message.getStr(CoreAdminParams.NAME);
     String target = message.getStr(CollectionAdminParams.TARGET);
@@ -64,7 +63,7 @@ public class RenameCmd implements CollApiCmds.CollectionApiCommand {
     } else {
       collectionName = extCollectionName;
     }
-    if (!state.hasCollection(collectionName)) {
+    if (!adminCmdContext.getClusterState().hasCollection(collectionName)) {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
           "source collection '" + collectionName + "' not found.");
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
index 5134e9953bd..382f7dd9e2e 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplaceNodeCmd.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.cloud.api.collections;
 
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -45,7 +43,7 @@ public class ReplaceNodeCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     ZkStateReader zkStateReader = ccc.getZkStateReader();
     String source = message.getStr(CollectionParams.SOURCE_NODE);
@@ -55,7 +53,6 @@ public class ReplaceNodeCmd implements CollApiCmds.CollectionApiCommand {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST, "sourceNode is a required param");
     }
-    String async = message.getStr(ASYNC);
     int timeout = message.getInt("timeout", 10 * 60); // 10 minutes
     boolean parallel = message.getBool("parallel", false);
     ClusterState clusterState = zkStateReader.getClusterState();
@@ -107,7 +104,7 @@ public class ReplaceNodeCmd implements CollApiCmds.CollectionApiCommand {
 
     boolean migrationSuccessful =
         ReplicaMigrationUtils.migrateReplicas(
-            ccc, replicaMovements, parallel, waitForFinalState, timeout, async, results);
+            ccc, adminCmdContext, replicaMovements, parallel, waitForFinalState, timeout, results);
     if (migrationSuccessful) {
       results.add(
           "success",
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplicaMigrationUtils.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplicaMigrationUtils.java
index d21aa3a5f95..498695f4f9f 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplicaMigrationUtils.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ReplicaMigrationUtils.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.cloud.api.collections;
 
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
@@ -37,6 +35,7 @@ import org.apache.solr.common.cloud.CollectionStateWatcher;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.zookeeper.KeeperException;
@@ -53,22 +52,22 @@ public class ReplicaMigrationUtils {
    * <p>If an error occurs during the creation of new replicas, all new replicas will be deleted.
    *
    * @param ccc The collection command context to use from the API that calls this method
+   * @param adminCmdContext The context of the originating admin request
    * @param movements a map from replica to the new node that the replica should live on
    * @param parallel whether the replica creations should be done in parallel
    * @param waitForFinalState wait for the final state of all newly created replicas before
    *     continuing
    * @param timeout the amount of time to wait for new replicas to be created
-   * @param asyncId If provided, the command will be run under the given asyncId
    * @param results push results (successful and failure) onto this list
    * @return whether the command was successful
    */
   static boolean migrateReplicas(
       CollectionCommandContext ccc,
+      AdminCmdContext adminCmdContext,
       Map<Replica, String> movements,
       boolean parallel,
       boolean waitForFinalState,
       int timeout,
-      String asyncId,
       NamedList<Object> results)
       throws IOException, InterruptedException, KeeperException {
     // how many leaders are we moving? for these replicas we have to make sure that either:
@@ -92,8 +91,6 @@ public class ReplicaMigrationUtils {
     SolrCloseableLatch replicasToRecover =
         new SolrCloseableLatch(numLeaders, ccc.getCloseableToLatchOn());
 
-    ClusterState clusterState = ccc.getZkStateReader().getClusterState();
-
     for (Map.Entry<Replica, String> movement : movements.entrySet()) {
       Replica sourceReplica = movement.getKey();
       String targetNode = movement.getValue();
@@ -111,12 +108,13 @@ public class ReplicaMigrationUtils {
               .toFullProps()
               .plus("parallel", String.valueOf(parallel))
               .plus(CoreAdminParams.NODE, targetNode);
-      if (asyncId != null) msg.getProperties().put(ASYNC, asyncId);
       NamedList<Object> nl = new NamedList<>();
       final ZkNodeProps addedReplica =
           new AddReplicaCmd(ccc)
               .addReplica(
-                  clusterState,
+                  adminCmdContext
+                      .subRequestContext(CollectionParams.CollectionAction.ADDREPLICA)
+                      .withClusterState(ccc.getZkStateReader().getClusterState()),
                   msg,
                   nl,
                   () -> {
@@ -213,7 +211,9 @@ public class ReplicaMigrationUtils {
         try {
           new DeleteReplicaCmd(ccc)
               .deleteReplica(
-                  ccc.getZkStateReader().getClusterState(),
+                  adminCmdContext
+                      .subRequestContext(CollectionParams.CollectionAction.DELETEREPLICA)
+                      .withClusterState(ccc.getZkStateReader().getClusterState()),
                   createdReplica.plus("parallel", "true"),
                   deleteResult,
                   () -> {
@@ -241,16 +241,14 @@ public class ReplicaMigrationUtils {
 
     // we have reached this far, meaning all replicas should have been recreated.
     // now cleanup the original replicas
-    return cleanupReplicas(
-        results, ccc.getZkStateReader().getClusterState(), movements.keySet(), ccc, asyncId);
+    return cleanupReplicas(results, adminCmdContext, movements.keySet(), ccc);
   }
 
   static boolean cleanupReplicas(
       NamedList<Object> results,
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       Collection<Replica> sourceReplicas,
-      CollectionCommandContext ccc,
-      String async)
+      CollectionCommandContext ccc)
       throws IOException, InterruptedException {
     SolrCloseableLatch cleanupLatch =
         new SolrCloseableLatch(sourceReplicas.size(), ccc.getCloseableToLatchOn());
@@ -268,10 +266,11 @@ public class ReplicaMigrationUtils {
       NamedList<Object> deleteResult = new NamedList<>();
       try {
         ZkNodeProps cmdMessage = sourceReplica.toFullProps();
-        if (async != null) cmdMessage = cmdMessage.plus(ASYNC, async);
         new DeleteReplicaCmd(ccc)
             .deleteReplica(
-                clusterState,
+                adminCmdContext
+                    .subRequestContext(CollectionParams.CollectionAction.DELETEREPLICA)
+                    .withClusterState(ccc.getZkStateReader().getClusterState()),
                 cmdMessage.plus("parallel", "true"),
                 deleteResult,
                 () -> {
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java
index 55d1725e6ec..93df952eb27 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/RestoreCmd.java
@@ -21,9 +21,10 @@ import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
 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.params.CollectionParams.CollectionAction.ADDREPLICA;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATESHARD;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CollectionParams.CollectionAction.MODIFYCOLLECTION;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
 import java.io.Closeable;
@@ -92,10 +93,10 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
-    try (RestoreContext restoreContext = new RestoreContext(message, ccc)) {
-      if (state.hasCollection(restoreContext.restoreCollectionName)) {
+    try (RestoreContext restoreContext = new RestoreContext(adminCmdContext, message, ccc)) {
+      if (adminCmdContext.getClusterState().hasCollection(restoreContext.restoreCollectionName)) {
         RestoreOnExistingCollection restoreOnExistingCollection =
             new RestoreOnExistingCollection(restoreContext);
         restoreOnExistingCollection.process(restoreContext, results);
@@ -111,14 +112,13 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
   private void requestReplicasToRestore(
       NamedList<Object> results,
       DocCollection restoreCollection,
-      ClusterState clusterState,
+      AdminCmdContext adminCmdContext,
       BackupProperties backupProperties,
       URI backupPath,
       String repo,
-      ShardHandler shardHandler,
-      String asyncId) {
+      ShardHandler shardHandler) {
     ShardRequestTracker shardRequestTracker =
-        CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+        CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
     // Copy data from backed up index to each replica
     for (Slice slice : restoreCollection.getSlices()) {
       ModifiableSolrParams params = new ModifiableSolrParams();
@@ -131,7 +131,8 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
       }
       params.set(CoreAdminParams.BACKUP_LOCATION, backupPath.toASCIIString());
       params.set(CoreAdminParams.BACKUP_REPOSITORY, repo);
-      shardRequestTracker.sliceCmd(clusterState, params, null, slice, shardHandler);
+      shardRequestTracker.sliceCmd(
+          adminCmdContext.getClusterState(), params, null, slice, shardHandler);
     }
     shardRequestTracker.processResponses(
         new NamedList<>(), shardHandler, true, "Could not restore core");
@@ -140,10 +141,10 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
   /** Encapsulates the parsing and access for common parameters restore parameters and values */
   private static class RestoreContext implements Closeable {
 
+    final AdminCmdContext adminCmdContext;
     final String restoreCollectionName;
     final String backupName;
     final String backupCollection;
-    final String asyncId;
     final String repo;
     final String restoreConfigName;
     final int backupId;
@@ -159,10 +160,12 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
     final DocCollection backupCollectionState;
     final ShardHandler shardHandler;
 
-    private RestoreContext(ZkNodeProps message, CollectionCommandContext ccc) throws IOException {
+    private RestoreContext(
+        AdminCmdContext adminCmdContext, ZkNodeProps message, CollectionCommandContext ccc)
+        throws IOException {
+      this.adminCmdContext = adminCmdContext;
       this.restoreCollectionName = message.getStr(COLLECTION_PROP);
       this.backupName = message.getStr(NAME); // of backup
-      this.asyncId = message.getStr(ASYNC);
       this.repo = message.getStr(CoreAdminParams.BACKUP_REPOSITORY);
       this.backupId = message.getInt(CoreAdminParams.BACKUP_ID, -1);
 
@@ -236,10 +239,10 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
           rc.backupName,
           rc.location);
       createCoreLessCollection(
+          rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()),
           rc.restoreCollectionName,
           rc.restoreConfigName,
-          rc.backupCollectionState,
-          rc.zkStateReader.getClusterState());
+          rc.backupCollectionState);
       // note: when createCollection() returns, the collection exists (no race)
 
       // Restore collection properties
@@ -248,8 +251,6 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
       DocCollection restoreCollection =
           rc.zkStateReader.getClusterState().getCollection(rc.restoreCollectionName);
       markAllShardsAsConstruction(restoreCollection);
-      // TODO how do we leverage the RULE / SNITCH logic in createCollection?
-      ClusterState clusterState = rc.zkStateReader.getClusterState();
 
       List<String> sliceNames = new ArrayList<>();
       restoreCollection.getSlices().forEach(x -> sliceNames.add(x.getName()));
@@ -258,11 +259,15 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
           getReplicaPositions(rc.restoreCollectionName, rc.nodeList, sliceNames);
 
       createSingleReplicaPerShard(
-          results, restoreCollection, rc.asyncId, clusterState, replicaPositions);
+          results,
+          restoreCollection,
+          rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()),
+          replicaPositions);
       Object failures = results.get("failure");
       if (failures != null && ((SimpleOrderedMap<?>) failures).size() > 0) {
         log.error("Restore failed to create initial replicas.");
-        CollectionHandlingUtils.cleanupCollection(rc.restoreCollectionName, new NamedList<>(), ccc);
+        CollectionHandlingUtils.cleanupCollection(
+            rc.adminCmdContext, rc.restoreCollectionName, new NamedList<>(), ccc);
         return;
       }
 
@@ -272,14 +277,17 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
       requestReplicasToRestore(
           results,
           restoreCollection,
-          clusterState,
+          rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()),
           rc.backupProperties,
           rc.backupPath,
           rc.repo,
-          rc.shardHandler,
-          rc.asyncId);
+          rc.shardHandler);
       markAllShardsAsActive(restoreCollection);
-      addReplicasToShards(results, clusterState, restoreCollection, replicaPositions, rc.asyncId);
+      addReplicasToShards(
+          results,
+          restoreCollection,
+          replicaPositions,
+          rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()));
       restoringAlias(rc.backupProperties);
 
       log.info("Completed restoring collection={} backupName={}", restoreCollection, rc.backupName);
@@ -310,10 +318,10 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
     }
 
     private void createCoreLessCollection(
+        AdminCmdContext adminCmdContext,
         String restoreCollectionName,
         String restoreConfigName,
-        DocCollection backupCollectionState,
-        ClusterState clusterState)
+        DocCollection backupCollectionState)
         throws Exception {
       Map<String, Object> propMap = new HashMap<>();
       propMap.put(Overseer.QUEUE_OPERATION, CREATE.toString());
@@ -368,7 +376,11 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
         propMap.put(CollectionHandlingUtils.SHARDS_PROP, newSlices);
       }
 
-      new CreateCollectionCmd(ccc).call(clusterState, new ZkNodeProps(propMap), new NamedList<>());
+      new CreateCollectionCmd(ccc)
+          .call(
+              adminCmdContext.subRequestContext(CREATE, null),
+              new ZkNodeProps(propMap),
+              new NamedList<>());
       // note: when createCollection() returns, the collection exists (no race)
     }
 
@@ -411,8 +423,7 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
     private void createSingleReplicaPerShard(
         NamedList<Object> results,
         DocCollection restoreCollection,
-        String asyncId,
-        ClusterState clusterState,
+        AdminCmdContext adminCmdContext,
         List<ReplicaPosition> replicaPositions)
         throws Exception {
       CountDownLatch countDownLatch = new CountDownLatch(restoreCollection.getSlices().size());
@@ -438,15 +449,11 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
           }
         }
 
-        // add async param
-        if (asyncId != null) {
-          propMap.put(ASYNC, asyncId);
-        }
         CollectionHandlingUtils.addPropertyParams(message, propMap);
         final NamedList<Object> addReplicaResult = new NamedList<>();
         new AddReplicaCmd(ccc)
             .addReplica(
-                clusterState,
+                adminCmdContext.subRequestContext(ADDREPLICA),
                 new ZkNodeProps(propMap),
                 addReplicaResult,
                 () -> {
@@ -508,10 +515,9 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
 
     private void addReplicasToShards(
         NamedList<Object> results,
-        ClusterState clusterState,
         DocCollection restoreCollection,
         List<ReplicaPosition> replicaPositions,
-        String asyncId)
+        AdminCmdContext adminCmdContext)
         throws Exception {
       int totalReplicasPerShard = numReplicas.total();
       if (totalReplicasPerShard > 1) {
@@ -558,14 +564,14 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
               }
             }
 
-            // add async param
-            if (asyncId != null) {
-              propMap.put(ASYNC, asyncId);
-            }
             CollectionHandlingUtils.addPropertyParams(message, propMap);
 
             new AddReplicaCmd(ccc)
-                .addReplica(clusterState, new ZkNodeProps(propMap), results, null);
+                .addReplica(
+                    adminCmdContext.subRequestContext(ADDREPLICA),
+                    new ZkNodeProps(propMap),
+                    results,
+                    null);
           }
         }
       }
@@ -616,23 +622,26 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
       ClusterState clusterState = rc.zkStateReader.getClusterState();
       DocCollection restoreCollection = clusterState.getCollection(rc.restoreCollectionName);
 
-      enableReadOnly(clusterState, restoreCollection);
+      enableReadOnly(
+          rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()),
+          restoreCollection);
       try {
         requestReplicasToRestore(
             results,
             restoreCollection,
-            clusterState,
+            rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()),
             rc.backupProperties,
             rc.backupPath,
             rc.repo,
-            rc.shardHandler,
-            rc.asyncId);
+            rc.shardHandler);
       } finally {
-        disableReadOnly(clusterState, restoreCollection);
+        disableReadOnly(
+            rc.adminCmdContext.withClusterState(rc.zkStateReader.getClusterState()),
+            restoreCollection);
       }
     }
 
-    private void disableReadOnly(ClusterState clusterState, DocCollection restoreCollection)
+    private void disableReadOnly(AdminCmdContext adminCmdContext, DocCollection restoreCollection)
         throws Exception {
       ZkNodeProps params =
           new ZkNodeProps(
@@ -640,10 +649,12 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
                   CollectionParams.CollectionAction.MODIFYCOLLECTION.toString(),
               ZkStateReader.COLLECTION_PROP, restoreCollection.getName(),
               ZkStateReader.READ_ONLY, null);
-      new CollApiCmds.ModifyCollectionCmd(ccc).call(clusterState, params, new NamedList<>());
+      new CollApiCmds.ModifyCollectionCmd(ccc)
+          .call(
+              adminCmdContext.subRequestContext(MODIFYCOLLECTION, null), params, new NamedList<>());
     }
 
-    private void enableReadOnly(ClusterState clusterState, DocCollection restoreCollection)
+    private void enableReadOnly(AdminCmdContext adminCmdContext, DocCollection restoreCollection)
         throws Exception {
       ZkNodeProps params =
           new ZkNodeProps(
@@ -651,7 +662,9 @@ public class RestoreCmd implements CollApiCmds.CollectionApiCommand {
                   CollectionParams.CollectionAction.MODIFYCOLLECTION.toString(),
               ZkStateReader.COLLECTION_PROP, restoreCollection.getName(),
               ZkStateReader.READ_ONLY, "true");
-      new CollApiCmds.ModifyCollectionCmd(ccc).call(clusterState, params, new NamedList<>());
+      new CollApiCmds.ModifyCollectionCmd(ccc)
+          .call(
+              adminCmdContext.subRequestContext(MODIFYCOLLECTION, null), params, new NamedList<>());
     }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/SetAliasPropCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/SetAliasPropCmd.java
index 671b71dddb6..f43e9ceeef2 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/SetAliasPropCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/SetAliasPropCmd.java
@@ -26,7 +26,6 @@ import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;
 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.cloud.ZkStateReader;
 import org.apache.solr.common.util.NamedList;
@@ -46,7 +45,7 @@ public class SetAliasPropCmd implements CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
     String aliasName = message.getStr(NAME);
 
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
index bf7f698d346..c2f0fd3d500 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
@@ -24,7 +24,6 @@ import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.CREATESHARD;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.DELETESHARD;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.NUM_SUB_SHARDS;
 
 import java.lang.invoke.MethodHandles;
@@ -101,9 +100,9 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   @Override
-  public void call(ClusterState state, ZkNodeProps message, NamedList<Object> results)
+  public void call(AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
-    split(state, message, results);
+    split(adminCmdContext, message, results);
   }
 
   /**
@@ -131,10 +130,9 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
    * <p>There is a shard split doc (dev-docs/shard-split/shard-split.adoc) on how shard split works;
    * illustrated with diagrams.
    */
-  public boolean split(ClusterState clusterState, ZkNodeProps message, NamedList<Object> results)
+  public boolean split(
+      AdminCmdContext adminCmdContext, ZkNodeProps message, NamedList<Object> results)
       throws Exception {
-    final String asyncId = message.getStr(ASYNC);
-
     boolean waitForFinalState = message.getBool(CommonAdminParams.WAIT_FOR_FINAL_STATE, false);
     String methodStr =
         message.getStr(
@@ -165,6 +163,7 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
     slice.set(message.getStr(ZkStateReader.SHARD_ID_PROP));
     Set<String> offlineSlices = new HashSet<>();
     RTimerTree timings = new RTimerTree();
+    ClusterState clusterState = zkStateReader.getClusterState();
 
     String splitKey = message.getStr("split.key");
     DocCollection collection = clusterState.getCollection(collectionName);
@@ -275,7 +274,7 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
 
         {
           final ShardRequestTracker shardRequestTracker =
-              CollectionHandlingUtils.syncRequestTracker(ccc);
+              CollectionHandlingUtils.syncRequestTracker(adminCmdContext, ccc);
           shardRequestTracker.sendShardRequest(
               parentShardLeader.getNodeName(), params, shardHandler);
           SimpleOrderedMap<Object> getRangesResults = new SimpleOrderedMap<>();
@@ -338,7 +337,13 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
             propMap.put(SHARD_ID_PROP, subSlice);
             ZkNodeProps m = new ZkNodeProps(propMap);
             try {
-              new DeleteShardCmd(ccc).call(clusterState, m, new NamedList<>());
+              new DeleteShardCmd(ccc)
+                  .call(
+                      adminCmdContext
+                          .subRequestContext(DELETESHARD, null)
+                          .withClusterState(clusterState),
+                      m,
+                      new NamedList<>());
             } catch (Exception e) {
               throw new SolrException(
                   SolrException.ErrorCode.SERVER_ERROR,
@@ -416,17 +421,17 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
             propMap.put(key, message.getStr(key));
           }
         }
-        // add async param
-        if (asyncId != null) {
-          propMap.put(ASYNC, asyncId);
-        }
         new AddReplicaCmd(ccc)
-            .addReplica(clusterState, new ZkNodeProps((MapWriter) propMap), results, null);
+            .addReplica(
+                adminCmdContext.subRequestContext(ADDREPLICA).withClusterState(clusterState),
+                new ZkNodeProps((MapWriter) propMap),
+                results,
+                null);
       }
 
       {
         final ShardRequestTracker syncRequestTracker =
-            CollectionHandlingUtils.syncRequestTracker(ccc);
+            CollectionHandlingUtils.syncRequestTracker(adminCmdContext, ccc);
         String msgOnError = "SPLITSHARD failed to create subshard leaders";
         syncRequestTracker.processResponses(results, shardHandler, true, msgOnError);
         handleFailureOnAsyncRequest(results, msgOnError);
@@ -437,7 +442,7 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
       t = timings.sub("waitForSubSliceLeadersAlive");
       {
         final ShardRequestTracker shardRequestTracker =
-            CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+            CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
         for (String subShardName : subShardNames) {
           // wait for parent leader to acknowledge the sub-shard core
           log.debug(
@@ -492,7 +497,7 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
       t = timings.sub("splitParentCore");
       {
         final ShardRequestTracker shardRequestTracker =
-            CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+            CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
         shardRequestTracker.sendShardRequest(parentShardLeader.getNodeName(), params, shardHandler);
 
         String msgOnError = "SPLITSHARD failed to invoke SPLIT core admin command";
@@ -509,7 +514,7 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
       t = timings.sub("applyBufferedUpdates");
       {
         final ShardRequestTracker shardRequestTracker =
-            CollectionHandlingUtils.asyncRequestTracker(asyncId, ccc);
+            CollectionHandlingUtils.asyncRequestTracker(adminCmdContext, ccc);
 
         for (int i = 0; i < subShardNames.size(); i++) {
           String subShardName = subShardNames.get(i);
@@ -627,10 +632,6 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
             propMap.put(key, message.getStr(key));
           }
         }
-        // add async param
-        if (asyncId != null) {
-          propMap.put(ASYNC, asyncId);
-        }
         // special flag param to instruct addReplica not to create the replica in cluster state
         // again
         propMap.put(CollectionHandlingUtils.SKIP_CREATE_REPLICA_IN_CLUSTER_STATE, "true");
@@ -775,14 +776,19 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
       t = timings.sub("createCoresForReplicas");
       // 11. now actually create replica cores on sub shard nodes
       for (Map<String, Object> replica : replicas) {
-        new AddReplicaCmd(ccc).addReplica(clusterState, new ZkNodeProps(replica), results, null);
+        new AddReplicaCmd(ccc)
+            .addReplica(
+                adminCmdContext.subRequestContext(ADDREPLICA).withClusterState(clusterState),
+                new ZkNodeProps(replica),
+                results,
+                null);
       }
 
       assert TestInjection.injectSplitFailureAfterReplicaCreation();
 
       {
         final ShardRequestTracker syncRequestTracker =
-            CollectionHandlingUtils.syncRequestTracker(ccc);
+            CollectionHandlingUtils.syncRequestTracker(adminCmdContext, ccc);
         String msgOnError = "SPLITSHARD failed to create subshard replicas";
         syncRequestTracker.processResponses(results, shardHandler, true, msgOnError);
         handleFailureOnAsyncRequest(results, msgOnError);
@@ -822,7 +828,12 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
     } finally {
       if (!success) {
         cleanupAfterFailure(
-            zkStateReader, collectionName, parentSlice.getName(), subSlices, offlineSlices);
+            adminCmdContext,
+            zkStateReader,
+            collectionName,
+            parentSlice.getName(),
+            subSlices,
+            offlineSlices);
         unlockForSplit(ccc.getSolrCloudManager(), collectionName, parentSlice.getName());
       }
     }
@@ -895,6 +906,7 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
   }
 
   private void cleanupAfterFailure(
+      AdminCmdContext adminCmdContext,
       ZkStateReader zkStateReader,
       String collectionName,
       String parentShard,
@@ -996,7 +1008,11 @@ public class SplitShardCmd implements CollApiCmds.CollectionApiCommand {
       props.put(SHARD_ID_PROP, subSlice);
       ZkNodeProps m = new ZkNodeProps(props);
       try {
-        new DeleteShardCmd(ccc).call(clusterState, m, new NamedList<Object>());
+        new DeleteShardCmd(ccc)
+            .call(
+                adminCmdContext.subRequestContext(DELETESHARD, null).withClusterState(clusterState),
+                m,
+                new NamedList<>());
       } catch (Exception e) {
         log.warn(
             "Cleanup failed after failed split of {}/{} : (deleting existing sub shard{})",
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 4102edff3e1..946559e5016 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
@@ -107,6 +107,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -138,6 +139,7 @@ import org.apache.solr.cloud.OverseerTaskQueue;
 import org.apache.solr.cloud.OverseerTaskQueue.QueueEvent;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.cloud.ZkController.NotInClusterStateException;
+import org.apache.solr.cloud.api.collections.AdminCmdContext;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils;
 import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
 import org.apache.solr.cloud.api.collections.ReindexCollectionCmd;
@@ -317,17 +319,13 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
       return;
     }
 
-    String asyncId = req.getParams().get(ASYNC);
-    if (asyncId != null) {
-      props.put(ASYNC, asyncId);
-    }
-
-    props.put(QUEUE_OPERATION, operation.action.toLower());
+    AdminCmdContext adminCmdContext =
+        new AdminCmdContext(operation.action, req.getParams().get(ASYNC));
 
     ZkNodeProps zkProps = new ZkNodeProps(props);
     final SolrResponse overseerResponse;
 
-    overseerResponse = submitCollectionApiCommand(zkProps, operation.action, operation.timeOut);
+    overseerResponse = submitCollectionApiCommand(adminCmdContext, zkProps, operation.timeOut);
 
     rsp.getValues().addAll(overseerResponse.getResponse());
     Exception exp = overseerResponse.getException();
@@ -340,13 +338,13 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
 
   public static long DEFAULT_COLLECTION_OP_TIMEOUT = 180 * 1000;
 
-  public SolrResponse submitCollectionApiCommand(ZkNodeProps m, CollectionAction action)
+  public SolrResponse submitCollectionApiCommand(AdminCmdContext adminCmdContext, ZkNodeProps m)
       throws KeeperException, InterruptedException {
-    return submitCollectionApiCommand(m, action, DEFAULT_COLLECTION_OP_TIMEOUT);
+    return submitCollectionApiCommand(adminCmdContext, m, DEFAULT_COLLECTION_OP_TIMEOUT);
   }
 
   public static SolrResponse submitCollectionApiCommand(
-      ZkController zkController, ZkNodeProps m, CollectionAction action, long timeout)
+      ZkController zkController, AdminCmdContext adminCmdContext, ZkNodeProps m, long timeout)
       throws KeeperException, InterruptedException {
     // Collection API messages are either sent to Overseer and processed there, or processed
     // locally. Distributing Collection API implies we're also distributing Cluster State Updates.
@@ -361,14 +359,17 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     Optional<DistributedCollectionConfigSetCommandRunner> distribCommandRunner =
         zkController.getDistributedCommandRunner();
     if (distribCommandRunner.isPresent()) {
-      return distribCommandRunner.get().runCollectionCommand(m, action, timeout);
+      return distribCommandRunner.get().runCollectionCommand(adminCmdContext, m, timeout);
     } else { // Sending the Collection API message to Overseer via a Zookeeper queue
-      String operation = m.getStr(QUEUE_OPERATION);
-      if (operation == null) {
-        throw new SolrException(ErrorCode.BAD_REQUEST, "missing key " + QUEUE_OPERATION);
+      String operation = adminCmdContext.getAction().lowerName;
+      HashMap<String, Object> additionalProps = new HashMap<>();
+      additionalProps.put(QUEUE_OPERATION, operation);
+      if (adminCmdContext.getAsyncId() != null && !adminCmdContext.getAsyncId().isBlank()) {
+        additionalProps.put(ASYNC, adminCmdContext.getAsyncId());
       }
-      if (m.get(ASYNC) != null) {
-        String asyncId = m.getStr(ASYNC);
+      m = m.plus(additionalProps);
+      if (adminCmdContext.getAsyncId() != null) {
+        String asyncId = adminCmdContext.getAsyncId();
         NamedList<Object> r = new NamedList<>();
 
         if (zkController.claimAsyncId(asyncId)) {
@@ -391,7 +392,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
           throw new SolrException(
               BAD_REQUEST, "Task with the same requestid already exists. (" + asyncId + ")");
         }
-        r.add(CoreAdminParams.REQUESTID, m.get(ASYNC));
+        r.add(CoreAdminParams.REQUESTID, asyncId);
 
         return new OverseerSolrResponse(r);
       }
@@ -426,9 +427,9 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
   }
 
   public SolrResponse submitCollectionApiCommand(
-      ZkNodeProps m, CollectionAction action, long timeout)
+      AdminCmdContext adminCmdContext, ZkNodeProps m, long timeout)
       throws KeeperException, InterruptedException {
-    return submitCollectionApiCommand(coreContainer.getZkController(), m, action, timeout);
+    return submitCollectionApiCommand(coreContainer.getZkController(), adminCmdContext, m, timeout);
   }
 
   private boolean overseerCollectionQueueContains(String asyncId)
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/RebalanceLeaders.java b/solr/core/src/java/org/apache/solr/handler/admin/RebalanceLeaders.java
index 2bebe2ba3ac..26afe771d5f 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/RebalanceLeaders.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/RebalanceLeaders.java
@@ -16,7 +16,6 @@
  */
 package org.apache.solr.handler.admin;
 
-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.CORE_NAME_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.CORE_NODE_NAME_PROP;
@@ -25,7 +24,6 @@ import static org.apache.solr.common.cloud.ZkStateReader.MAX_AT_ONCE_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.MAX_WAIT_SECONDS_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REJOIN_AT_HEAD_PROP;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.REBALANCELEADERS;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 
 import java.lang.invoke.MethodHandles;
 import java.util.HashMap;
@@ -38,6 +36,7 @@ import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.apache.solr.cloud.LeaderElector;
 import org.apache.solr.cloud.OverseerTaskProcessor;
+import org.apache.solr.cloud.api.collections.AdminCmdContext;
 import org.apache.solr.cloud.overseer.SliceMutator;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
@@ -46,7 +45,6 @@ import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.CoreContainer;
@@ -444,10 +442,8 @@ class RebalanceLeaders {
       Slice slice, String electionNode, String core, boolean rejoinAtHead)
       throws KeeperException, InterruptedException {
     Replica replica = slice.getReplica(LeaderElector.getNodeName(electionNode));
-    final CollectionParams.CollectionAction rebalanceleaders = REBALANCELEADERS;
     Map<String, Object> propMap = new HashMap<>();
     propMap.put(COLLECTION_PROP, collectionName);
-    propMap.put(QUEUE_OPERATION, rebalanceleaders.toLower());
     propMap.put(CORE_NAME_PROP, core);
     propMap.put(CORE_NODE_NAME_PROP, replica.getName());
     propMap.put(ZkStateReader.NODE_NAME_PROP, replica.getNodeName());
@@ -459,12 +455,12 @@ class RebalanceLeaders {
             .getBaseUrlForNodeName(replica.getNodeName()));
     propMap.put(
         REJOIN_AT_HEAD_PROP, Boolean.toString(rejoinAtHead)); // Get ourselves to be first in line.
-    String asyncId = rebalanceleaders.toLower() + "_" + core + "_" + Math.abs(System.nanoTime());
-    propMap.put(ASYNC, asyncId);
+    String asyncId = REBALANCELEADERS.toLower() + "_" + core + "_" + Math.abs(System.nanoTime());
     asyncRequests.add(asyncId);
 
     collectionsHandler.submitCollectionApiCommand(
-        new ZkNodeProps(propMap), rebalanceleaders); // ignore response; we construct our own
+        new AdminCmdContext(REBALANCELEADERS, asyncId),
+        new ZkNodeProps(propMap)); // ignore response; we construct our own
   }
 
   // maxWaitSecs - How long are we going to wait? Defaults to 30 seconds.
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaProperty.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaProperty.java
index 81b2dbce137..863a4a84d21 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaProperty.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaProperty.java
@@ -16,7 +16,6 @@
  */
 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.SHARD_UNIQUE;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.PROPERTY_PROP;
@@ -24,7 +23,6 @@ import static org.apache.solr.common.cloud.ZkStateReader.PROPERTY_VALUE_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -34,13 +32,11 @@ import java.util.Map;
 import org.apache.solr.client.api.endpoint.AddReplicaPropertyApi;
 import org.apache.solr.client.api.model.AddReplicaPropertyRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.cloud.overseer.SliceMutator;
 import org.apache.solr.common.SolrException;
 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.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -73,20 +69,13 @@ public class AddReplicaProperty extends AdminAPIBase implements AddReplicaProper
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
     }
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
 
     final ZkNodeProps remoteMessage =
         createRemoteMessage(collName, shardName, replicaName, propertyName, requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.ADDREPLICAPROP,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+    submitRemoteMessageAndHandleException(
+        response, CollectionParams.CollectionAction.ADDREPLICAPROP, remoteMessage);
 
     disableResponseCaching();
     return response;
@@ -104,7 +93,6 @@ public class AddReplicaProperty extends AdminAPIBase implements AddReplicaProper
     remoteMessage.put(SHARD_ID_PROP, shardName);
     remoteMessage.put(REPLICA_PROP, replicaName);
     remoteMessage.put(PROPERTY_VALUE_PROP, requestBody.value);
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.ADDREPLICAPROP.toLower());
     if (requestBody.shardUnique != null) {
       remoteMessage.put(SHARD_UNIQUE, requestBody.shardUnique);
     }
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 c4e001a3ad3..3d627c4cfed 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
@@ -21,8 +21,11 @@ import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTIO
 
 import java.util.Map;
 import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.cloud.api.collections.AdminCmdContext;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.ZkNodeProps;
@@ -70,11 +73,7 @@ public abstract class AdminAPIBase extends JerseyResource {
   }
 
   private String resolveAlias(String aliasName) {
-    return coreContainer
-        .getZkController()
-        .getZkStateReader()
-        .getAliases()
-        .resolveSimpleAlias(aliasName);
+    return coreContainer.getAliases().resolveSimpleAlias(aliasName);
   }
 
   public static void validateZooKeeperAwareCoreContainer(CoreContainer coreContainer) {
@@ -92,13 +91,7 @@ public abstract class AdminAPIBase extends JerseyResource {
 
   protected String resolveCollectionName(String collName, boolean followAliases) {
     final String collectionName =
-        followAliases
-            ? coreContainer
-                .getZkController()
-                .getZkStateReader()
-                .getAliases()
-                .resolveSimpleAlias(collName)
-            : collName;
+        followAliases ? coreContainer.getAliases().resolveSimpleAlias(collName) : collName;
 
     final ClusterState clusterState = coreContainer.getZkController().getClusterState();
     if (!clusterState.hasCollection(collectionName)) {
@@ -124,18 +117,55 @@ public abstract class AdminAPIBase extends JerseyResource {
     solrQueryResponse.setHttpCaching(false);
   }
 
-  protected SolrResponse submitRemoteMessageAndHandleResponse(
-      SubResponseAccumulatingJerseyResponse response,
-      CollectionParams.CollectionAction action,
-      ZkNodeProps remoteMessage,
-      String asyncId)
+  protected SolrResponse submitRemoteMessageAndHandleException(
+      SolrJerseyResponse response, AdminCmdContext adminCmdContext, ZkNodeProps remoteMessage)
       throws Exception {
     final SolrResponse remoteResponse =
         CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(), remoteMessage, action, DEFAULT_COLLECTION_OP_TIMEOUT);
+            coreContainer.getZkController(),
+            adminCmdContext,
+            remoteMessage,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
     if (remoteResponse.getException() != null) {
       throw remoteResponse.getException();
     }
+    return remoteResponse;
+  }
+
+  protected SolrResponse submitRemoteMessageAndHandleException(
+      SolrJerseyResponse response,
+      CollectionParams.CollectionAction action,
+      ZkNodeProps remoteMessage)
+      throws Exception {
+    return submitRemoteMessageAndHandleException(
+        response, new AdminCmdContext(action, null), remoteMessage);
+  }
+
+  protected SolrResponse submitRemoteMessageAndHandleAsync(
+      AsyncJerseyResponse response,
+      CollectionParams.CollectionAction action,
+      ZkNodeProps remoteMessage,
+      String asyncId)
+      throws Exception {
+    var remoteResponse =
+        submitRemoteMessageAndHandleException(
+            response, new AdminCmdContext(action, asyncId), remoteMessage);
+
+    if (asyncId != null) {
+      response.requestId = asyncId;
+    }
+
+    return remoteResponse;
+  }
+
+  protected SolrResponse submitRemoteMessageAndHandleResponse(
+      SubResponseAccumulatingJerseyResponse response,
+      CollectionParams.CollectionAction action,
+      ZkNodeProps remoteMessage,
+      String asyncId)
+      throws Exception {
+    var remoteResponse =
+        submitRemoteMessageAndHandleAsync(response, action, remoteMessage, asyncId);
 
     if (asyncId != null) {
       response.requestId = asyncId;
@@ -144,6 +174,7 @@ public abstract class AdminAPIBase extends JerseyResource {
     // Values fetched from remoteResponse may be null
     response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
     response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
+    response.warning = (String) remoteResponse.getResponse().get("warning");
 
     return remoteResponse;
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/AliasProperty.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AliasProperty.java
index 56af9177a96..077c7ee4217 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/AliasProperty.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AliasProperty.java
@@ -16,30 +16,27 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
 
 import jakarta.inject.Inject;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.AliasPropertyApis;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
 import org.apache.solr.client.api.model.GetAliasPropertyResponse;
 import org.apache.solr.client.api.model.GetAllAliasPropertiesResponse;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UpdateAliasPropertiesRequestBody;
 import org.apache.solr.client.api.model.UpdateAliasPropertyRequestBody;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.Aliases;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
 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.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -103,7 +100,7 @@ public class AliasProperty extends AdminAPIBase implements AliasPropertyApis {
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse updateAliasProperties(
+  public AsyncJerseyResponse updateAliasProperties(
       String aliasName, UpdateAliasPropertiesRequestBody requestBody) throws Exception {
 
     if (requestBody == null) {
@@ -112,8 +109,8 @@ public class AliasProperty extends AdminAPIBase implements AliasPropertyApis {
 
     recordCollectionForLogAndTracing(null, solrQueryRequest);
 
-    SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    modifyAliasProperties(aliasName, requestBody.properties, requestBody.async);
+    var response = instantiateJerseyResponse(AsyncJerseyResponse.class);
+    modifyAliasProperties(response, aliasName, requestBody.properties, requestBody.async);
     return response;
   }
 
@@ -128,8 +125,8 @@ public class AliasProperty extends AdminAPIBase implements AliasPropertyApis {
 
     recordCollectionForLogAndTracing(null, solrQueryRequest);
 
-    SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    modifyAliasProperty(aliasName, propName, requestBody.value);
+    var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    modifyAliasProperty(response, aliasName, propName, requestBody.value);
     return response;
   }
 
@@ -139,52 +136,50 @@ public class AliasProperty extends AdminAPIBase implements AliasPropertyApis {
       throws Exception {
     recordCollectionForLogAndTracing(null, solrQueryRequest);
 
-    SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    modifyAliasProperty(aliasName, propName, null);
+    var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    modifyAliasProperty(response, aliasName, propName, null);
     return response;
   }
 
-  private void modifyAliasProperty(String alias, String proertyName, Object value)
+  private static final String PROPERTIES = "property";
+
+  private void modifyAliasProperty(
+      SolrJerseyResponse response, String alias, String proertyName, Object value)
       throws Exception {
     Map<String, Object> props = new HashMap<>();
     // value can be null
     props.put(proertyName, value);
-    modifyAliasProperties(alias, props, null);
+
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    submitRemoteMessageAndHandleException(
+        response,
+        CollectionParams.CollectionAction.ALIASPROP,
+        new ZkNodeProps(
+            Map.of(
+                NAME, alias,
+                PROPERTIES, props)));
+
+    disableResponseCaching();
   }
 
-  /**
-   * @param alias alias
-   */
-  private void modifyAliasProperties(String alias, Map<String, Object> properties, String async)
+  private void modifyAliasProperties(
+      AsyncJerseyResponse response, String alias, Map<String, Object> properties, String async)
       throws Exception {
     // Note: success/no-op in the event of no properties supplied is intentional. Keeps code
     // simple and one less case for api-callers to check for.
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
-    final ZkNodeProps remoteMessage = createRemoteMessage(alias, properties, async);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.ALIASPROP,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
+    if (properties == null) {
+      properties = Collections.emptyMap();
     }
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    submitRemoteMessageAndHandleAsync(
+        response,
+        CollectionParams.CollectionAction.ALIASPROP,
+        new ZkNodeProps(
+            Map.of(
+                NAME, alias,
+                PROPERTIES, properties)),
+        async);
 
     disableResponseCaching();
   }
-
-  private static final String PROPERTIES = "property";
-
-  public ZkNodeProps createRemoteMessage(
-      String alias, Map<String, Object> properties, String async) {
-    final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.ALIASPROP.toLower());
-    remoteMessage.put(NAME, alias);
-    remoteMessage.put(PROPERTIES, properties);
-    if (async != null) {
-      remoteMessage.put(ASYNC, async);
-    }
-    return new ZkNodeProps(remoteMessage);
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java
index b582e39464b..8ee0a7e4e77 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java
@@ -16,11 +16,8 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionParams.NODES;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -28,12 +25,10 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.BalanceReplicasApi;
 import org.apache.solr.client.api.model.BalanceReplicasRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -51,21 +46,16 @@ public class BalanceReplicas extends AdminAPIBase implements BalanceReplicasApi
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse balanceReplicas(BalanceReplicasRequestBody requestBody)
-      throws Exception {
-    final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
-    // TODO Record node for log and tracing
-    final ZkNodeProps remoteMessage = createRemoteMessage(requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionAction.BALANCE_REPLICAS,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+  public SubResponseAccumulatingJerseyResponse balanceReplicas(
+      BalanceReplicasRequestBody requestBody) throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionAction.BALANCE_REPLICAS,
+        createRemoteMessage(requestBody),
+        requestBody.async);
 
     disableResponseCaching();
     return response;
@@ -76,9 +66,7 @@ public class BalanceReplicas extends AdminAPIBase implements BalanceReplicasApi
     if (requestBody != null) {
       insertIfNotNull(remoteMessage, NODES, requestBody.nodes);
       insertIfNotNull(remoteMessage, WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
-      insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
     }
-    remoteMessage.put(QUEUE_OPERATION, CollectionAction.BALANCE_REPLICAS.toLower());
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUnique.java b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUnique.java
index ae54b0ab98d..ecaa1b9fa80 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUnique.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceShardUnique.java
@@ -16,7 +16,6 @@
  */
 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_ACTIVE_NODES;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.SHARD_UNIQUE;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
@@ -70,11 +69,10 @@ public class BalanceShardUnique extends AdminAPIBase implements BalanceShardUniq
     fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, requestBody);
     submitRemoteMessageAndHandleResponse(
         response,
         CollectionParams.CollectionAction.BALANCESHARDUNIQUE,
-        remoteMessage,
+        createRemoteMessage(collectionName, requestBody),
         requestBody.async);
 
     return response;
@@ -83,13 +81,10 @@ public class BalanceShardUnique extends AdminAPIBase implements BalanceShardUniq
   public static ZkNodeProps createRemoteMessage(
       String collectionName, BalanceShardUniqueRequestBody requestBody) {
     final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(
-        QUEUE_OPERATION, CollectionParams.CollectionAction.BALANCESHARDUNIQUE.toLower());
     remoteMessage.put(COLLECTION_PROP, collectionName);
     remoteMessage.put(PROPERTY_PROP, requestBody.property);
     insertIfNotNull(remoteMessage, ONLY_ACTIVE_NODES, requestBody.onlyActiveNodes);
     insertIfNotNull(remoteMessage, SHARD_UNIQUE, requestBody.shardUnique);
-    insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java
index bbeafb210e5..ac054f118b7 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java
@@ -18,7 +18,6 @@
 package org.apache.solr.handler.admin.api;
 
 import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.COLLECTIONS;
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.cloud.api.collections.RoutedAlias.CATEGORY;
 import static org.apache.solr.cloud.api.collections.RoutedAlias.CREATE_COLLECTION_PREFIX;
 import static org.apache.solr.cloud.api.collections.RoutedAlias.ROUTER_TYPE_NAME;
@@ -30,7 +29,6 @@ import static org.apache.solr.common.params.CollectionAdminParams.ROUTER_PREFIX;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.apache.solr.common.params.CommonParams.START;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -40,13 +38,11 @@ import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.CreateAliasApi;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
 import org.apache.solr.client.api.model.CategoryRoutedAliasProperties;
 import org.apache.solr.client.api.model.CreateAliasRequestBody;
 import org.apache.solr.client.api.model.RoutedAliasProperties;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.client.api.model.TimeRoutedAliasProperties;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.request.RoutedAliasTypes;
 import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
 import org.apache.solr.cloud.api.collections.RoutedAlias;
@@ -61,7 +57,6 @@ import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.CollectionUtil;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -83,9 +78,8 @@ public class CreateAlias extends AdminAPIBase implements CreateAliasApi {
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse createAlias(CreateAliasRequestBody requestBody) throws Exception {
-    final SubResponseAccumulatingJerseyResponse response =
-        instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+  public AsyncJerseyResponse createAlias(CreateAliasRequestBody requestBody) throws Exception {
+    final var response = instantiateJerseyResponse(AsyncJerseyResponse.class);
     recordCollectionForLogAndTracing(null, solrQueryRequest);
 
     if (requestBody == null) {
@@ -109,19 +103,8 @@ public class CreateAlias extends AdminAPIBase implements CreateAliasApi {
       remoteMessage = createRemoteMessageForRoutedAlias(requestBody);
     }
 
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.CREATEALIAS,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    if (requestBody.async != null) {
-      response.requestId = requestBody.async;
-    }
+    submitRemoteMessageAndHandleAsync(
+        response, CollectionParams.CollectionAction.CREATEALIAS, remoteMessage, requestBody.async);
     return response;
   }
 
@@ -129,19 +112,15 @@ public class CreateAlias extends AdminAPIBase implements CreateAliasApi {
       CreateAliasRequestBody requestBody) {
     final Map<String, Object> remoteMessage = new HashMap<>();
 
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATEALIAS.toLower());
     remoteMessage.put(NAME, requestBody.name);
     remoteMessage.put("collections", String.join(",", requestBody.collections));
-    remoteMessage.put(ASYNC, requestBody.async);
 
     return new ZkNodeProps(remoteMessage);
   }
 
   public static ZkNodeProps createRemoteMessageForRoutedAlias(CreateAliasRequestBody requestBody) {
     final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATEALIAS.toLower());
     remoteMessage.put(NAME, requestBody.name);
-    if (StrUtils.isNotBlank(requestBody.async)) remoteMessage.put(ASYNC, requestBody.async);
 
     if (requestBody.routers.size() > 1) { // Multi-dimensional alias
       for (int i = 0; i < requestBody.routers.size(); i++) {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollection.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollection.java
index fef25cfdb52..18c95583275 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollection.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollection.java
@@ -19,7 +19,6 @@ package org.apache.solr.handler.admin.api;
 
 import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.ROUTER_KEY;
 import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.SHARD_NAMES;
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREATE_NODE_SET;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREATE_NODE_SET_SHUFFLE;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.NUM_SLICES;
@@ -36,7 +35,6 @@ import static org.apache.solr.common.params.CollectionAdminParams.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.apache.solr.common.params.CoreAdminParams.NAME;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.handler.admin.CollectionsHandler.waitForActiveCollection;
 import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
@@ -70,7 +68,6 @@ import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.CollectionUtil;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -99,8 +96,7 @@ public class CreateCollection extends AdminAPIBase implements CreateCollectionAp
       throw new SolrException(BAD_REQUEST, "Request body is missing but required");
     }
 
-    final SubResponseAccumulatingJerseyResponse response =
-        instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
     final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(requestBody.name, solrQueryRequest);
 
@@ -109,26 +105,12 @@ public class CreateCollection extends AdminAPIBase implements CreateCollectionAp
     // Populate any 'null' creation parameters that support COLLECTIONPROP defaults.
     populateDefaultsIfNecessary(coreContainer, requestBody);
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(requestBody);
     final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
+        submitRemoteMessageAndHandleResponse(
+            response,
             CollectionParams.CollectionAction.CREATE,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    if (requestBody.async != null) {
-      response.requestId = requestBody.async;
-      return response;
-    }
-
-    // Values fetched from remoteResponse may be null
-    response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
-    response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
-    response.warning = (String) remoteResponse.getResponse().get("warning");
+            createRemoteMessage(requestBody),
+            requestBody.async);
 
     // Even if Overseer does wait for the collection to be created, it sees a different cluster
     // state than this node, so this wait is required to make sure the local node Zookeeper watches
@@ -163,7 +145,6 @@ public class CreateCollection extends AdminAPIBase implements CreateCollectionAp
     final Map<String, Object> rawProperties = new HashMap<>();
     rawProperties.put("fromApi", "true");
 
-    rawProperties.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATE.toLower());
     rawProperties.put(NAME, reqBody.name);
     rawProperties.put(COLL_CONF, reqBody.config);
     rawProperties.put(NUM_SLICES, reqBody.numShards);
@@ -176,7 +157,6 @@ public class CreateCollection extends AdminAPIBase implements CreateCollectionAp
     rawProperties.put(WAIT_FOR_FINAL_STATE, reqBody.waitForFinalState);
     rawProperties.put(PER_REPLICA_STATE, reqBody.perReplicaState);
     rawProperties.put(ALIAS, reqBody.alias);
-    rawProperties.put(ASYNC, reqBody.async);
     if (reqBody.createReplicas == null || reqBody.createReplicas) {
       // The remote message expects a single comma-delimited string, so nodeSet requires flattening
       if (reqBody.nodeSet != null) {
@@ -233,7 +213,7 @@ public class CreateCollection extends AdminAPIBase implements CreateCollectionAp
   private static Integer readIntegerDefaultFromClusterProp(
       CoreContainer coreContainer, String propName) throws IOException {
     final Object defaultValue =
-        new ClusterProperties(coreContainer.getZkController().getZkStateReader().getZkClient())
+        new ClusterProperties(coreContainer.getZkController().getZkClient())
             .getClusterProperty(
                 List.of(CollectionAdminParams.DEFAULTS, CollectionAdminParams.COLLECTION, propName),
                 null);
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackup.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackup.java
index 602bda58e7e..5e856012819 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackup.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackup.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionAdminParams.INDEX_BACKUP_STRATEGY;
@@ -30,7 +29,6 @@ import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
 import static org.apache.solr.common.params.CoreAdminParams.COMMIT_NAME;
 import static org.apache.solr.common.params.CoreAdminParams.MAX_NUM_BACKUP_POINTS;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.handler.admin.api.CreateCollection.copyPrefixedPropertiesWithoutPrefix;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
@@ -50,7 +48,6 @@ import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.jersey.SolrJacksonMapper;
 import org.apache.solr.request.SolrQueryRequest;
@@ -109,20 +106,12 @@ public class CreateCollectionBackup extends BackupAPIBase implements CollectionB
           "Unknown index backup strategy " + requestBody.backupStrategy);
     }
 
+    final var response = instantiateJerseyResponse(CreateCollectionBackupResponseBody.class);
     final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, backupName, requestBody);
     final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.BACKUP,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    final SolrJerseyResponse response =
-        objectMapper.convertValue(
-            remoteResponse.getResponse(), CreateCollectionBackupResponseBody.class);
+        submitRemoteMessageAndHandleResponse(
+            response, CollectionParams.CollectionAction.BACKUP, remoteMessage, requestBody.async);
+    objectMapper.updateValue(response, remoteResponse.getResponse());
 
     return response;
   }
@@ -130,7 +119,7 @@ public class CreateCollectionBackup extends BackupAPIBase implements CollectionB
   public static ZkNodeProps createRemoteMessage(
       String collectionName, String backupName, CreateCollectionBackupRequestBody requestBody) {
     final Map<String, Object> remoteMessage = Utils.reflectToMap(requestBody);
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.BACKUP.toLower());
+    remoteMessage.remove(ASYNC);
     remoteMessage.put(COLLECTION_PROP, collectionName);
     remoteMessage.put(NAME, backupName);
     if (!StringUtils.isBlank(requestBody.backupStrategy)) {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionSnapshot.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionSnapshot.java
index 96cf56ff3f6..2cfa244c74a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionSnapshot.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionSnapshot.java
@@ -16,11 +16,8 @@
  */
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.FOLLOW_ALIASES;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -29,7 +26,6 @@ import java.util.Map;
 import org.apache.solr.client.api.endpoint.CollectionSnapshotApis;
 import org.apache.solr.client.api.model.CreateCollectionSnapshotRequestBody;
 import org.apache.solr.client.api.model.CreateCollectionSnapshotResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkNodeProps;
@@ -37,7 +33,6 @@ import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.snapshots.SolrSnapshotManager;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -60,8 +55,7 @@ public class CreateCollectionSnapshot extends AdminAPIBase
   public CreateCollectionSnapshotResponse createCollectionSnapshot(
       String collName, String snapshotName, CreateCollectionSnapshotRequestBody requestBody)
       throws Exception {
-    final CreateCollectionSnapshotResponse response =
-        instantiateJerseyResponse(CreateCollectionSnapshotResponse.class);
+    final var response = instantiateJerseyResponse(CreateCollectionSnapshotResponse.class);
     final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
 
@@ -78,36 +72,26 @@ public class CreateCollectionSnapshot extends AdminAPIBase
               + "', no action taken.");
     }
 
-    final ZkNodeProps remoteMessage =
-        createRemoteMessage(collName, requestBody.followAliases, snapshotName, requestBody.async);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.CREATESNAPSHOT,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.CREATESNAPSHOT,
+        createRemoteMessage(collName, requestBody.followAliases, snapshotName),
+        requestBody.async);
 
     response.collection = collName;
     response.followAliases = requestBody.followAliases;
     response.snapshotName = snapshotName;
-    response.requestId = requestBody.async;
 
     return response;
   }
 
   public static ZkNodeProps createRemoteMessage(
-      String collectionName, boolean followAliases, String snapshotName, String asyncId) {
+      String collectionName, boolean followAliases, String snapshotName) {
     final Map<String, Object> remoteMessage = new HashMap<>();
 
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATESNAPSHOT.toLower());
     remoteMessage.put(COLLECTION_PROP, collectionName);
     remoteMessage.put(CoreAdminParams.COMMIT_NAME, snapshotName);
     remoteMessage.put(FOLLOW_ALIASES, followAliases);
-    if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateReplica.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateReplica.java
index 72c8eb058b1..a8bdc0e49ab 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateReplica.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateReplica.java
@@ -17,7 +17,6 @@
 
 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.CREATE_NODE_SET;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
@@ -90,17 +89,17 @@ public class CreateReplica extends AdminAPIBase implements CreateReplicaApi {
         resolveAndValidateAliasIfEnabled(
             collectionName, Boolean.TRUE.equals(requestBody.followAliases));
 
-    final ZkNodeProps remoteMessage =
-        createRemoteMessage(resolvedCollectionName, shardName, requestBody);
     submitRemoteMessageAndHandleResponse(
-        response, CollectionParams.CollectionAction.ADDREPLICA, remoteMessage, requestBody.async);
+        response,
+        CollectionParams.CollectionAction.ADDREPLICA,
+        createRemoteMessage(resolvedCollectionName, shardName, requestBody),
+        requestBody.async);
     return response;
   }
 
   public static ZkNodeProps createRemoteMessage(
       String collectionName, String shardName, CreateReplicaRequestBody requestBody) {
     final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.ADDREPLICA.toLower());
     remoteMessage.put(COLLECTION_PROP, collectionName);
     remoteMessage.put(SHARD_ID_PROP, shardName);
     insertIfNotNull(remoteMessage, CoreAdminParams.NAME, requestBody.name);
@@ -119,7 +118,6 @@ public class CreateReplica extends AdminAPIBase implements CreateReplicaApi {
     insertIfNotNull(remoteMessage, TLOG_REPLICAS, requestBody.tlogReplicas);
     insertIfNotNull(remoteMessage, PULL_REPLICAS, requestBody.pullReplicas);
     insertIfNotNull(remoteMessage, FOLLOW_ALIASES, requestBody.followAliases);
-    insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
 
     if (requestBody.properties != null) {
       requestBody
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShard.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShard.java
index 2912fbd8e07..6a3b02192ed 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShard.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShard.java
@@ -17,7 +17,6 @@
 
 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.CREATE_NODE_SET;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
@@ -90,9 +89,11 @@ public class CreateShard extends AdminAPIBase implements CreateShardApi {
             collectionName, Boolean.TRUE.equals(requestBody.followAliases));
     ensureCollectionUsesImplicitRouter(resolvedCollectionName);
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(resolvedCollectionName, requestBody);
     submitRemoteMessageAndHandleResponse(
-        response, CollectionParams.CollectionAction.CREATESHARD, remoteMessage, requestBody.async);
+        response,
+        CollectionParams.CollectionAction.CREATESHARD,
+        createRemoteMessage(resolvedCollectionName, requestBody),
+        requestBody.async);
     return response;
   }
 
@@ -137,7 +138,6 @@ public class CreateShard extends AdminAPIBase implements CreateShardApi {
   public static ZkNodeProps createRemoteMessage(
       String collectionName, CreateShardRequestBody requestBody) {
     final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATESHARD.toLower());
     remoteMessage.put(COLLECTION_PROP, collectionName);
     remoteMessage.put(SHARD_ID_PROP, requestBody.shardName);
     if (requestBody.createReplicas == null || requestBody.createReplicas) {
@@ -154,7 +154,6 @@ public class CreateShard extends AdminAPIBase implements CreateShardApi {
     insertIfNotNull(remoteMessage, PULL_REPLICAS, requestBody.pullReplicas);
     insertIfNotNull(remoteMessage, WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
     insertIfNotNull(remoteMessage, FOLLOW_ALIASES, requestBody.followAliases);
-    insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
 
     if (requestBody.properties != null) {
       requestBody
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAlias.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAlias.java
index aac01f61c84..d0edbf6a77d 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAlias.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAlias.java
@@ -17,23 +17,16 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
-import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.DeleteAliasApi;
 import org.apache.solr.client.api.model.AsyncJerseyResponse;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 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.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -49,34 +42,15 @@ public class DeleteAlias extends AdminAPIBase implements DeleteAliasApi {
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse deleteAlias(String aliasName, String asyncId) throws Exception {
-    final AsyncJerseyResponse response = instantiateJerseyResponse(AsyncJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
-
-    final ZkNodeProps remoteMessage = createRemoteMessage(aliasName, asyncId);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.DELETEALIAS,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    if (asyncId != null) {
-      response.requestId = asyncId;
-    }
-
+  public AsyncJerseyResponse deleteAlias(String aliasName, String asyncId) throws Exception {
+    final var response = instantiateJerseyResponse(AsyncJerseyResponse.class);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+
+    submitRemoteMessageAndHandleAsync(
+        response,
+        CollectionParams.CollectionAction.DELETEALIAS,
+        new ZkNodeProps(Map.of(NAME, aliasName)),
+        asyncId);
     return response;
   }
-
-  public static ZkNodeProps createRemoteMessage(String aliasName, String asyncId) {
-    final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETEALIAS.toLower());
-    remoteMessage.put(NAME, aliasName);
-    if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
-
-    return new ZkNodeProps(remoteMessage);
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollection.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollection.java
index 3b6d87ac33c..ac327aad77a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollection.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollection.java
@@ -16,11 +16,8 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 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.CommonParams.NAME;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -28,11 +25,9 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.DeleteCollectionApi;
 import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 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.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -57,42 +52,24 @@ public class DeleteCollection extends AdminAPIBase implements DeleteCollectionAp
   @PermissionName(COLL_EDIT_PERM)
   public SubResponseAccumulatingJerseyResponse deleteCollection(
       String collectionName, Boolean followAliases, String asyncId) throws Exception {
-    final SubResponseAccumulatingJerseyResponse response =
-        instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, followAliases, asyncId);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.DELETE,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    if (asyncId != null) {
-      response.requestId = asyncId;
-      return response;
-    }
-
-    // Values fetched from remoteResponse may be null
-    response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
-    response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.DELETE,
+        createRemoteMessage(collectionName, followAliases),
+        asyncId);
 
     return response;
   }
 
-  public static ZkNodeProps createRemoteMessage(
-      String collectionName, Boolean followAliases, String asyncId) {
+  public static ZkNodeProps createRemoteMessage(String collectionName, Boolean followAliases) {
     final Map<String, Object> remoteMessage = new HashMap<>();
 
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETE.toLower());
     remoteMessage.put(NAME, collectionName);
     if (followAliases != null) remoteMessage.put(FOLLOW_ALIASES, followAliases);
-    if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionBackup.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionBackup.java
index bb88c09853c..a2612984123 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionBackup.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionBackup.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_ID;
@@ -87,7 +86,7 @@ public class DeleteCollectionBackup extends BackupAPIBase implements DeleteColle
     location = getAndValidateBackupLocation(repositoryName, location);
 
     final ZkNodeProps remoteMessage =
-        createRemoteMessage(backupName, backupId, null, null, location, repositoryName, asyncId);
+        createRemoteMessage(backupName, backupId, null, null, location, repositoryName);
     final var remoteResponse =
         submitRemoteMessageAndHandleResponse(
             response, CollectionParams.CollectionAction.DELETEBACKUP, remoteMessage, asyncId);
@@ -113,8 +112,7 @@ public class DeleteCollectionBackup extends BackupAPIBase implements DeleteColle
     location = getAndValidateBackupLocation(repositoryName, location);
 
     final ZkNodeProps remoteMessage =
-        createRemoteMessage(
-            backupName, null, versionsToRetain, null, location, repositoryName, asyncId);
+        createRemoteMessage(backupName, null, versionsToRetain, null, location, repositoryName);
     final var remoteResponse =
         submitRemoteMessageAndHandleResponse(
             response, CollectionParams.CollectionAction.DELETEBACKUP, remoteMessage, asyncId);
@@ -139,13 +137,7 @@ public class DeleteCollectionBackup extends BackupAPIBase implements DeleteColle
 
     final ZkNodeProps remoteMessage =
         createRemoteMessage(
-            backupName,
-            null,
-            null,
-            Boolean.TRUE,
-            requestBody.location,
-            requestBody.repositoryName,
-            requestBody.async);
+            backupName, null, null, Boolean.TRUE, requestBody.location, requestBody.repositoryName);
     final var remoteResponse =
         submitRemoteMessageAndHandleResponse(
             response,
@@ -167,12 +159,10 @@ public class DeleteCollectionBackup extends BackupAPIBase implements DeleteColle
       Integer versionsToRetain,
       Boolean purgeUnused,
       String location,
-      String repositoryName,
-      String asyncId) {
+      String repositoryName) {
     final Map<String, Object> remoteMessage = new HashMap<>();
 
     // Always provided
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETEBACKUP.toLower());
     remoteMessage.put(NAME, backupName);
     // Mutually exclusive
     assert backupId != null || versionsToRetain != null || purgeUnused != null;
@@ -182,7 +172,6 @@ public class DeleteCollectionBackup extends BackupAPIBase implements DeleteColle
     // Remaining params are truly optional
     insertIfNotNull(remoteMessage, BACKUP_LOCATION, location);
     insertIfNotNull(remoteMessage, BACKUP_REPOSITORY, repositoryName);
-    insertIfNotNull(remoteMessage, ASYNC, asyncId);
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionSnapshot.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionSnapshot.java
index dec728026a3..086fde0cff0 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionSnapshot.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionSnapshot.java
@@ -16,11 +16,8 @@
  */
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.FOLLOW_ALIASES;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -28,12 +25,10 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.CollectionSnapshotApis;
 import org.apache.solr.client.api.model.DeleteCollectionSnapshotResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -56,23 +51,16 @@ public class DeleteCollectionSnapshot extends AdminAPIBase
       String collName, String snapshotName, boolean followAliases, String asyncId)
       throws Exception {
     final var response = instantiateJerseyResponse(DeleteCollectionSnapshotResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
 
     final String collectionName = resolveCollectionName(collName, followAliases);
 
-    final ZkNodeProps remoteMessage =
-        createRemoteMessage(collectionName, followAliases, snapshotName, asyncId);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.DELETESNAPSHOT,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.DELETESNAPSHOT,
+        createRemoteMessage(collectionName, followAliases, snapshotName),
+        asyncId);
 
     response.collection = collName;
     response.snapshotName = snapshotName;
@@ -83,16 +71,13 @@ public class DeleteCollectionSnapshot extends AdminAPIBase
   }
 
   public static ZkNodeProps createRemoteMessage(
-      String collectionName, boolean followAliases, String snapshotName, String asyncId) {
+      String collectionName, boolean followAliases, String snapshotName) {
     final Map<String, Object> remoteMessage = new HashMap<>();
 
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETESNAPSHOT.toLower());
     remoteMessage.put(COLLECTION_PROP, collectionName);
     remoteMessage.put(CoreAdminParams.COMMIT_NAME, snapshotName);
     remoteMessage.put(FOLLOW_ALIASES, followAliases);
 
-    if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
-
     return new ZkNodeProps(remoteMessage);
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNode.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNode.java
index 78af96b4ec2..43c95ad3cb5 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNode.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNode.java
@@ -16,25 +16,21 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CoreAdminParams.NODE;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
-import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.DeleteNodeApi;
 import org.apache.solr.client.api.model.DeleteNodeRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.RequiredSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -57,20 +53,15 @@ public class DeleteNode extends AdminAPIBase implements DeleteNodeApi {
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse deleteNode(String nodeName, DeleteNodeRequestBody requestBody)
-      throws Exception {
-    final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
-    final ZkNodeProps remoteMessage = createRemoteMessage(nodeName, requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.DELETENODE,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+  public SubResponseAccumulatingJerseyResponse deleteNode(
+      String nodeName, DeleteNodeRequestBody requestBody) throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.DELETENODE,
+        new ZkNodeProps(Map.of(NODE, nodeName)),
+        requestBody != null ? requestBody.async : null);
     disableResponseCaching();
     return response;
   }
@@ -82,18 +73,4 @@ public class DeleteNode extends AdminAPIBase implements DeleteNodeApi {
     requestBody.async = params.get(ASYNC);
     return apiInstance.deleteNode(requiredParams.get(NODE), requestBody);
   }
-
-  public static ZkNodeProps createRemoteMessage(
-      String nodeName, DeleteNodeRequestBody requestBody) {
-    Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(NODE, nodeName);
-    if (requestBody != null) {
-      if (requestBody.async != null) {
-        remoteMessage.put(ASYNC, requestBody.async);
-      }
-    }
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETENODE.toLower());
-
-    return new ZkNodeProps(remoteMessage);
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplica.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplica.java
index 857624b4852..2e6a2d648a9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplica.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplica.java
@@ -17,7 +17,6 @@
 
 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.SolrException.ErrorCode.BAD_REQUEST;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
@@ -92,8 +91,7 @@ public class DeleteReplica extends AdminAPIBase implements DeleteReplicaApi {
             deleteInstanceDir,
             deleteDataDir,
             deleteIndex,
-            onlyIfDown,
-            async);
+            onlyIfDown);
     submitRemoteMessageAndHandleResponse(
         response, CollectionParams.CollectionAction.DELETEREPLICA, remoteMessage, async);
     return response;
@@ -129,8 +127,7 @@ public class DeleteReplica extends AdminAPIBase implements DeleteReplicaApi {
             deleteInstanceDir,
             deleteDataDir,
             deleteIndex,
-            onlyIfDown,
-            asyncId);
+            onlyIfDown);
     submitRemoteMessageAndHandleResponse(
         response, CollectionParams.CollectionAction.DELETEREPLICA, remoteMessage, asyncId);
     return response;
@@ -172,8 +169,7 @@ public class DeleteReplica extends AdminAPIBase implements DeleteReplicaApi {
             requestBody.deleteInstanceDir,
             requestBody.deleteDataDir,
             requestBody.deleteIndex,
-            requestBody.onlyIfDown,
-            requestBody.async);
+            requestBody.onlyIfDown);
     submitRemoteMessageAndHandleResponse(
         response,
         CollectionParams.CollectionAction.DELETEREPLICA,
@@ -191,10 +187,8 @@ public class DeleteReplica extends AdminAPIBase implements DeleteReplicaApi {
       Boolean deleteInstanceDir,
       Boolean deleteDataDir,
       Boolean deleteIndex,
-      Boolean onlyIfDown,
-      String asyncId) {
+      Boolean onlyIfDown) {
     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);
@@ -204,7 +198,6 @@ public class DeleteReplica extends AdminAPIBase implements DeleteReplicaApi {
     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);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaProperty.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaProperty.java
index 9c2486fdc39..f5bb319d377 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaProperty.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteReplicaProperty.java
@@ -17,25 +17,21 @@
 
 package org.apache.solr.handler.admin.api;
 
-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.PROPERTY_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 
 import jakarta.inject.Inject;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.DeleteReplicaPropertyApi;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.RequiredSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -61,20 +57,13 @@ public class DeleteReplicaProperty extends AdminAPIBase implements DeleteReplica
   public SolrJerseyResponse deleteReplicaProperty(
       String collName, String shardName, String replicaName, String propertyName) throws Exception {
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
 
-    final ZkNodeProps remoteMessage =
-        createRemoteMessage(collName, shardName, replicaName, propertyName);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.DELETEREPLICAPROP,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+    submitRemoteMessageAndHandleException(
+        response,
+        CollectionParams.CollectionAction.DELETEREPLICAPROP,
+        createRemoteMessage(collName, shardName, replicaName, propertyName));
 
     return response;
   }
@@ -99,8 +88,6 @@ public class DeleteReplicaProperty extends AdminAPIBase implements DeleteReplica
       String collName, String shardName, String replicaName, String propName) {
     final Map<String, Object> messageProperties =
         Map.of(
-            QUEUE_OPERATION,
-            CollectionParams.CollectionAction.DELETEREPLICAPROP.toLower(),
             COLLECTION_PROP,
             collName,
             SHARD_ID_PROP,
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShard.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShard.java
index 5b9b5407274..8ef2c35966d 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShard.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteShard.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-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;
@@ -80,8 +79,7 @@ public class DeleteShard extends AdminAPIBase implements DeleteShardApi {
             deleteInstanceDir,
             deleteDataDir,
             deleteIndex,
-            followAliases,
-            asyncId);
+            followAliases);
     submitRemoteMessageAndHandleResponse(
         response, CollectionParams.CollectionAction.DELETESHARD, remoteMessage, asyncId);
     return response;
@@ -93,17 +91,14 @@ public class DeleteShard extends AdminAPIBase implements DeleteShardApi {
       Boolean deleteInstanceDir,
       Boolean deleteDataDir,
       Boolean deleteIndex,
-      Boolean followAliases,
-      String asyncId) {
+      Boolean followAliases) {
     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);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/InstallShardData.java b/solr/core/src/java/org/apache/solr/handler/admin/api/InstallShardData.java
index 98623f7d209..840bbeab4a5 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/InstallShardData.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/InstallShardData.java
@@ -17,15 +17,13 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
 import java.util.HashMap;
 import org.apache.solr.client.api.endpoint.InstallShardDataApi;
-import org.apache.solr.client.api.model.AsyncJerseyResponse;
 import org.apache.solr.client.api.model.InstallShardDataRequestBody;
-import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.cloud.api.collections.InstallShardDataCmd;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
@@ -35,7 +33,6 @@ import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -59,9 +56,9 @@ public class InstallShardData extends AdminAPIBase implements InstallShardDataAp
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public AsyncJerseyResponse installShardData(
+  public SubResponseAccumulatingJerseyResponse installShardData(
       String collName, String shardName, InstallShardDataRequestBody requestBody) throws Exception {
-    final var response = instantiateJerseyResponse(AsyncJerseyResponse.class);
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
     final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
     if (requestBody == null) {
@@ -87,21 +84,11 @@ public class InstallShardData extends AdminAPIBase implements InstallShardDataAp
           "Collection must be in readOnly mode before installing data to shard");
     }
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(collName, shardName, requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.INSTALLSHARDDATA,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    if (requestBody.async != null) {
-      response.requestId = requestBody.async;
-      return response;
-    }
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.INSTALLSHARDDATA,
+        createRemoteMessage(collName, shardName, requestBody),
+        requestBody.async);
 
     return response;
   }
@@ -125,7 +112,6 @@ public class InstallShardData extends AdminAPIBase implements InstallShardDataAp
     if (requestBody != null) {
       messageTyped.location = requestBody.location;
       messageTyped.repository = requestBody.repository;
-      messageTyped.asyncId = requestBody.async;
     }
 
     messageTyped.validate();
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateReplicas.java b/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateReplicas.java
index 670540b4f74..ad9b9604080 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateReplicas.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/MigrateReplicas.java
@@ -16,12 +16,9 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionParams.SOURCE_NODES;
 import static org.apache.solr.common.params.CollectionParams.TARGET_NODES;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -29,13 +26,11 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.MigrateReplicasApi;
 import org.apache.solr.client.api.model.MigrateReplicasRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -53,21 +48,16 @@ public class MigrateReplicas extends AdminAPIBase implements MigrateReplicasApi
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse migrateReplicas(MigrateReplicasRequestBody requestBody)
-      throws Exception {
-    final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
-    // TODO Record node for log and tracing
-    final ZkNodeProps remoteMessage = createRemoteMessage(requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionAction.MIGRATE_REPLICAS,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+  public SubResponseAccumulatingJerseyResponse migrateReplicas(
+      MigrateReplicasRequestBody requestBody) throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionAction.MIGRATE_REPLICAS,
+        createRemoteMessage(requestBody),
+        requestBody.async);
 
     disableResponseCaching();
     return response;
@@ -84,13 +74,11 @@ public class MigrateReplicas extends AdminAPIBase implements MigrateReplicasApi
       insertIfNotNull(remoteMessage, SOURCE_NODES, requestBody.sourceNodes);
       insertIfNotNull(remoteMessage, TARGET_NODES, requestBody.targetNodes);
       insertIfNotNull(remoteMessage, WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
-      insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
     } else {
       throw new SolrException(
           SolrException.ErrorCode.BAD_REQUEST,
           "No request body sent with request. The MigrateReplicas API requires a body.");
     }
-    remoteMessage.put(QUEUE_OPERATION, CollectionAction.MIGRATE_REPLICAS.toLower());
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java
index b620dd430a7..227d944e8e3 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ReloadCollectionAPI.java
@@ -16,7 +16,6 @@
  */
 package org.apache.solr.handler.admin.api;
 
-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.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
@@ -24,7 +23,6 @@ import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PER
 
 import jakarta.inject.Inject;
 import java.lang.invoke.MethodHandles;
-import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.ReloadCollectionApi;
 import org.apache.solr.client.api.model.ReloadCollectionRequestBody;
@@ -66,27 +64,14 @@ public class ReloadCollectionAPI extends AdminAPIBase implements ReloadCollectio
     fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, requestBody);
     submitRemoteMessageAndHandleResponse(
         response,
         CollectionParams.CollectionAction.RELOAD,
-        remoteMessage,
+        new ZkNodeProps(Map.of(NAME, collectionName)),
         requestBody != null ? requestBody.async : null);
     return response;
   }
 
-  public static ZkNodeProps createRemoteMessage(
-      String collectionName, ReloadCollectionRequestBody requestBody) {
-    final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.RELOAD.toLower());
-    remoteMessage.put(NAME, collectionName);
-    if (requestBody != null) {
-      insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
-    }
-
-    return new ZkNodeProps(remoteMessage);
-  }
-
   public static void invokeFromV1Params(
       CoreContainer coreContainer, SolrQueryRequest request, SolrQueryResponse response)
       throws Exception {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java b/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java
index 1a6ed3a3946..6562ae1d872 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java
@@ -16,7 +16,6 @@
  */
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionAdminParams.TARGET;
@@ -69,21 +68,16 @@ public class RenameCollection extends AdminAPIBase implements RenameCollectionAp
 
     final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, requestBody);
     submitRemoteMessageAndHandleResponse(
-        response,
-        CollectionParams.CollectionAction.RENAME,
-        remoteMessage,
-        requestBody != null ? requestBody.async : null);
+        response, CollectionParams.CollectionAction.RENAME, remoteMessage, requestBody.async);
     return response;
   }
 
   public static ZkNodeProps createRemoteMessage(
       String collectionName, RenameCollectionRequestBody requestBody) {
     final Map<String, Object> remoteMessage = new HashMap<>();
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.RENAME.toLower());
     remoteMessage.put(NAME, collectionName);
     remoteMessage.put(TARGET, requestBody.to);
     insertIfNotNull(remoteMessage, FOLLOW_ALIASES, requestBody.followAliases);
-    insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java
index bb36f2036df..cd162659087 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java
@@ -16,12 +16,9 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionParams.SOURCE_NODE;
 import static org.apache.solr.common.params.CollectionParams.TARGET_NODE;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -29,13 +26,10 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.solr.client.api.endpoint.ReplaceNodeApi;
 import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -57,21 +51,16 @@ public class ReplaceNode extends AdminAPIBase implements ReplaceNodeApi {
 
   @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse replaceNode(String sourceNodeName, ReplaceNodeRequestBody requestBody)
-      throws Exception {
-    final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
-    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
-    // TODO Record node for log and tracing
-    final ZkNodeProps remoteMessage = createRemoteMessage(sourceNodeName, requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.REPLACENODE,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
+  public SubResponseAccumulatingJerseyResponse replaceNode(
+      String sourceNodeName, ReplaceNodeRequestBody requestBody) throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.REPLACENODE,
+        createRemoteMessage(sourceNodeName, requestBody),
+        requestBody != null ? requestBody.async : null);
 
     disableResponseCaching();
     return response;
@@ -83,9 +72,7 @@ public class ReplaceNode extends AdminAPIBase implements ReplaceNodeApi {
     if (requestBody != null) {
       insertIfValueNotNull(remoteMessage, TARGET_NODE, requestBody.targetNodeName);
       insertIfValueNotNull(remoteMessage, WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
-      insertIfValueNotNull(remoteMessage, ASYNC, requestBody.async);
     }
-    remoteMessage.put(QUEUE_OPERATION, CollectionAction.REPLACENODE.toLower());
 
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCollection.java b/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCollection.java
index fb39fafa02e..435d04236f9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCollection.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/RestoreCollection.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.COLL_CONF;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
@@ -29,7 +28,6 @@ import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_ID;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
-import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
 import jakarta.inject.Inject;
@@ -41,7 +39,6 @@ import org.apache.solr.client.api.endpoint.CollectionBackupApi;
 import org.apache.solr.client.api.model.RestoreCollectionRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
-import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils;
 import org.apache.solr.common.SolrException;
@@ -51,7 +48,6 @@ import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -113,6 +109,7 @@ public class RestoreCollection extends BackupAPIBase implements CollectionBackup
 
     final var createRequestBody = requestBody.createCollectionParams;
     if (createRequestBody != null) {
+      createRequestBody.name = collectionName;
       CreateCollection.populateDefaultsIfNecessary(coreContainer, createRequestBody);
       CreateCollection.validateRequestBody(createRequestBody);
       if (Boolean.FALSE.equals(createRequestBody.createReplicas)) {
@@ -122,32 +119,18 @@ public class RestoreCollection extends BackupAPIBase implements CollectionBackup
       }
     }
 
-    final ZkNodeProps remoteMessage = createRemoteMessage(backupName, requestBody);
-    final SolrResponse remoteResponse =
-        CollectionsHandler.submitCollectionApiCommand(
-            coreContainer.getZkController(),
-            remoteMessage,
-            CollectionParams.CollectionAction.RESTORE,
-            DEFAULT_COLLECTION_OP_TIMEOUT);
-
-    if (remoteResponse.getException() != null) {
-      throw remoteResponse.getException();
-    }
-
-    if (requestBody.async != null) {
-      response.requestId = requestBody.async;
-      return response;
-    }
-
-    // Values fetched from remoteResponse may be null
-    response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
-    response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.RESTORE,
+        createRemoteMessage(backupName, requestBody),
+        requestBody.async);
     return response;
   }
 
   public ZkNodeProps createRemoteMessage(
       String backupName, RestoreCollectionRequestBody requestBody) {
     final Map<String, Object> remoteMessage = Utils.reflectToMap(requestBody);
+    remoteMessage.remove(ASYNC);
 
     // If the RESTORE is setup to create a new collection, copy those parameters first
     final var createReqBody = requestBody.createCollectionParams;
@@ -164,7 +147,6 @@ public class RestoreCollection extends BackupAPIBase implements CollectionBackup
     }
 
     // Copy restore-specific parameters
-    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.RESTORE.toLower());
     remoteMessage.put(NAME, backupName);
     return new ZkNodeProps(remoteMessage);
   }
diff --git a/solr/core/src/java/org/apache/solr/util/TimeOut.java b/solr/core/src/java/org/apache/solr/util/TimeOut.java
index a1d682a6ed9..6fdc9bd2609 100644
--- a/solr/core/src/java/org/apache/solr/util/TimeOut.java
+++ b/solr/core/src/java/org/apache/solr/util/TimeOut.java
@@ -18,6 +18,7 @@ package org.apache.solr.util;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import java.time.Duration;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Supplier;
@@ -30,6 +31,14 @@ public class TimeOut {
   private final long timeoutAt, startTime;
   private final TimeSource timeSource;
 
+  /**
+   * @param timeout after this maximum time, {@link #hasTimedOut()} will return true.
+   * @param timeSource the source of the time.
+   */
+  public TimeOut(Duration timeout, TimeSource timeSource) {
+    this(timeout.toNanos(), NANOSECONDS, timeSource);
+  }
+
   /**
    * @param timeout after this maximum time, {@link #hasTimedOut()} will return true.
    * @param unit the time unit of the timeout argument.
diff --git a/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java b/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java
index ab2548e30fa..a64c485a9c7 100644
--- a/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/OverseerCollectionConfigSetProcessorTest.java
@@ -60,6 +60,7 @@ import org.apache.solr.cloud.OverseerTaskQueue.QueueEvent;
 import org.apache.solr.cloud.api.collections.CollectionHandlingUtils;
 import org.apache.solr.cluster.placement.PlacementPluginFactory;
 import org.apache.solr.cluster.placement.plugins.SimplePlacementFactory;
+import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.cloud.Aliases;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
@@ -621,22 +622,20 @@ public class OverseerCollectionConfigSetProcessorTest extends SolrTestCaseJ4 {
           .record(any(), any());
     } else {
       // Mocking for state change via the Overseer queue
+      Mockito.doCallRealMethod().when(overseerMock).offerStateUpdate((MapWriter) any());
       Mockito.doAnswer(
-              new Answer<Void>() {
-                @Override
-                public Void answer(InvocationOnMock invocation) {
-                  try {
-                    handleCreateCollMessage(invocation.getArgument(0));
-                    verify(stateUpdateQueueMock, Mockito.atLeast(0))
-                        .offer((byte[]) invocation.getArgument(0));
-                  } catch (KeeperException e) {
-                    throw new RuntimeException(e);
-                  } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                    throw new RuntimeException(e);
-                  }
-                  return null;
+              invocation -> {
+                try {
+                  handleCreateCollMessage(invocation.getArgument(0));
+                  verify(stateUpdateQueueMock, Mockito.atLeast(0))
+                      .offer((byte[]) invocation.getArgument(0));
+                } catch (KeeperException e) {
+                  throw new RuntimeException(e);
+                } catch (InterruptedException e) {
+                  Thread.currentThread().interrupt();
+                  throw new RuntimeException(e);
                 }
+                return null;
               })
           .when(overseerMock)
           .offerStateUpdate((byte[]) any());
diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionApiLockingTest.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionApiLockingTest.java
index fb943f6a5e6..7f6f7e7daf4 100644
--- a/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionApiLockingTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionApiLockingTest.java
@@ -65,7 +65,10 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // hierarchy)
     DistributedMultiLock collLock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.COLLECTION, COLLECTION_NAME, null, null);
+            new AdminCmdContext(CollectionParams.CollectionAction.CREATE),
+            COLLECTION_NAME,
+            null,
+            null);
     assertTrue("Collection should have been acquired", collLock.isAcquired());
     assertEquals(
         "Lock at collection level expected to need one distributed lock",
@@ -76,7 +79,10 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // above
     DistributedMultiLock shard1Lock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD1_NAME, null);
+            new AdminCmdContext(CollectionParams.CollectionAction.SPLITSHARD),
+            COLLECTION_NAME,
+            SHARD1_NAME,
+            null);
     assertFalse("Shard1 should not have been acquired", shard1Lock.isAcquired());
     assertEquals(
         "Lock at shard level expected to need two distributed locks",
@@ -87,7 +93,10 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // collection lock above
     DistributedMultiLock shard2Lock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD2_NAME, null);
+            new AdminCmdContext(CollectionParams.CollectionAction.SPLITSHARD),
+            COLLECTION_NAME,
+            SHARD2_NAME,
+            null);
     assertFalse("Shard2 should not have been acquired", shard2Lock.isAcquired());
 
     assertTrue("Collection should still be acquired", collLock.isAcquired());
@@ -104,7 +113,10 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // Request a lock on replica of shard1
     DistributedMultiLock replicaShard1Lock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD1_NAME, REPLICA_NAME);
+            new AdminCmdContext(CollectionParams.CollectionAction.MOCK_REPLICA_TASK),
+            COLLECTION_NAME,
+            SHARD1_NAME,
+            REPLICA_NAME);
     assertFalse(
         "replicaShard1Lock should not have been acquired, shard1 is locked",
         replicaShard1Lock.isAcquired());
@@ -112,7 +124,10 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // Now ask for a new lock on the collection
     collLock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.COLLECTION, COLLECTION_NAME, null, null);
+            new AdminCmdContext(CollectionParams.CollectionAction.CREATE),
+            COLLECTION_NAME,
+            null,
+            null);
 
     assertFalse(
         "Collection should not have been acquired, shard1 and shard2 locks preventing it",
@@ -131,7 +146,10 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // Request a lock on replica of shard2
     DistributedMultiLock replicaShard2Lock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD2_NAME, REPLICA_NAME);
+            new AdminCmdContext(CollectionParams.CollectionAction.MOCK_REPLICA_TASK),
+            COLLECTION_NAME,
+            SHARD2_NAME,
+            REPLICA_NAME);
     assertFalse(
         "replicaShard2Lock should not have been acquired, shard2 is locked",
         replicaShard2Lock.isAcquired());
@@ -158,13 +176,19 @@ public class CollectionApiLockingTest extends SolrTestCaseJ4 {
     // Lock on collection...
     DistributedMultiLock collLock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.COLLECTION, COLLECTION_NAME, null, null);
+            new AdminCmdContext(CollectionParams.CollectionAction.CREATE),
+            COLLECTION_NAME,
+            null,
+            null);
     assertTrue("Collection should have been acquired", collLock.isAcquired());
 
     // ...blocks a lock on replica from being acquired
     final DistributedMultiLock replicaShard1Lock =
         apiLockingHelper.createCollectionApiLock(
-            CollectionParams.LockLevel.SHARD, COLLECTION_NAME, SHARD1_NAME, REPLICA_NAME);
+            new AdminCmdContext(CollectionParams.CollectionAction.MOCK_REPLICA_TASK),
+            COLLECTION_NAME,
+            SHARD1_NAME,
+            REPLICA_NAME);
     assertFalse(
         "replicaShard1Lock should not have been acquired, because collection is locked",
         replicaShard1Lock.isAcquired());
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/AddReplicaPropertyAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/AddReplicaPropertyAPITest.java
index 07874f26486..8c91233d890 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/AddReplicaPropertyAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/AddReplicaPropertyAPITest.java
@@ -17,70 +17,29 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.SHARD_UNIQUE;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import io.opentelemetry.api.trace.Span;
-import java.util.Map;
-import java.util.Optional;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.AddReplicaPropertyRequestBody;
-import org.apache.solr.cloud.OverseerSolrResponse;
-import org.apache.solr.cloud.ZkController;
-import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ZkNodeProps;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.core.CoreContainer;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.common.params.CollectionParams;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /** Unit tests for {@link AddReplicaProperty} */
-public class AddReplicaPropertyAPITest extends SolrTestCaseJ4 {
+public class AddReplicaPropertyAPITest extends MockV2APITest {
 
   private static final AddReplicaPropertyRequestBody ANY_REQ_BODY =
       new AddReplicaPropertyRequestBody("anyValue");
 
-  private CoreContainer mockCoreContainer;
-  private ZkController mockZkController;
-  private DistributedCollectionConfigSetCommandRunner mockCommandRunner;
-  private SolrQueryRequest mockQueryRequest;
-  private SolrQueryResponse queryResponse;
-  private ArgumentCaptor<ZkNodeProps> messageCapturer;
-
-  private AddReplicaProperty addReplicaPropApi;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
-  }
+  private AddReplicaProperty api;
 
   @Override
   @Before
   public void setUp() throws Exception {
     super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
 
-    mockCoreContainer = mock(CoreContainer.class);
-    mockZkController = mock(ZkController.class);
-    mockCommandRunner = mock(DistributedCollectionConfigSetCommandRunner.class);
-    when(mockCoreContainer.getZkController()).thenReturn(mockZkController);
-    when(mockZkController.getDistributedCommandRunner()).thenReturn(Optional.of(mockCommandRunner));
-    when(mockCommandRunner.runCollectionCommand(any(), any(), anyLong()))
-        .thenReturn(new OverseerSolrResponse(new NamedList<>()));
-    mockQueryRequest = mock(SolrQueryRequest.class);
-    when(mockQueryRequest.getSpan()).thenReturn(Span.getInvalid());
-    queryResponse = new SolrQueryResponse();
-    messageCapturer = ArgumentCaptor.forClass(ZkNodeProps.class);
-
-    addReplicaPropApi = new AddReplicaProperty(mockCoreContainer, mockQueryRequest, queryResponse);
+    api = new AddReplicaProperty(mockCoreContainer, mockQueryRequest, queryResponse);
   }
 
   @Test
@@ -91,7 +50,7 @@ public class AddReplicaPropertyAPITest extends SolrTestCaseJ4 {
         expectThrows(
             SolrException.class,
             () -> {
-              addReplicaPropApi.addReplicaProperty(
+              api.addReplicaProperty(
                   "someColl", "someShard", "someReplica", "somePropName", ANY_REQ_BODY);
             });
     assertEquals(400, e.code());
@@ -104,21 +63,17 @@ public class AddReplicaPropertyAPITest extends SolrTestCaseJ4 {
   public void testCreatesValidOverseerMessage() throws Exception {
     when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
 
-    addReplicaPropApi.addReplicaProperty(
-        "someColl", "someShard", "someReplica", "somePropName", ANY_REQ_BODY);
-    verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
-
-    final ZkNodeProps createdMessage = messageCapturer.getValue();
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(6, createdMessageProps.size());
-    assertEquals("addreplicaprop", createdMessageProps.get("operation"));
-    assertEquals("someColl", createdMessageProps.get("collection"));
-    assertEquals("someShard", createdMessageProps.get("shard"));
-    assertEquals("someReplica", createdMessageProps.get("replica"));
-    assertEquals("somePropName", createdMessageProps.get("property"));
-    assertEquals("anyValue", createdMessageProps.get("property.value"));
-    assertFalse(
-        createdMessageProps.containsKey(
-            SHARD_UNIQUE)); // Omitted since not specified on request body
+    api.addReplicaProperty("someColl", "someShard", "someReplica", "somePropName", ANY_REQ_BODY);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.ADDREPLICAPROP,
+        message -> {
+          assertEquals(5, message.size());
+          assertEquals("someColl", message.get("collection"));
+          assertEquals("someShard", message.get("shard"));
+          assertEquals("someReplica", message.get("replica"));
+          assertEquals("somePropName", message.get("property"));
+          assertEquals("anyValue", message.get("property.value"));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java
index 2e5af32ff5d..8bb81e676cb 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/BackupCoreAPITest.java
@@ -38,7 +38,7 @@ import org.junit.Test;
 
 public class BackupCoreAPITest extends SolrTestCaseJ4 {
 
-  private CreateCoreBackup backupCoreAPI;
+  private CreateCoreBackup api;
   private static final String backupName = "my-new-backup";
 
   @BeforeClass
@@ -57,7 +57,7 @@ public class BackupCoreAPITest extends SolrTestCaseJ4 {
 
     CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker =
         new CoreAdminHandler.CoreAdminAsyncTracker();
-    backupCoreAPI =
+    api =
         new CreateCoreBackup(
             coreContainer, solrQueryRequest, solrQueryResponse, coreAdminAsyncTracker);
   }
@@ -68,8 +68,7 @@ public class BackupCoreAPITest extends SolrTestCaseJ4 {
     backupCoreRequestBody.incremental = false;
     backupCoreRequestBody.backupName = backupName;
     SnapShooter.CoreSnapshotResponse response =
-        (SnapShooter.CoreSnapshotResponse)
-            backupCoreAPI.createBackup(coreName, backupCoreRequestBody);
+        (SnapShooter.CoreSnapshotResponse) api.createBackup(coreName, backupCoreRequestBody);
 
     assertEquals(backupName, response.snapshotName);
     assertEquals("snapshot." + backupName, response.directoryName);
@@ -84,11 +83,7 @@ public class BackupCoreAPITest extends SolrTestCaseJ4 {
     backupCoreRequestBody.incremental = false;
     backupCoreRequestBody.backupName = backupName;
     final SolrException solrException =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              backupCoreAPI.createBackup(coreName, backupCoreRequestBody);
-            });
+        expectThrows(SolrException.class, () -> api.createBackup(coreName, backupCoreRequestBody));
     assertEquals(500, solrException.code());
     assertTrue(
         "Exception message differed from expected: " + solrException.getMessage(),
@@ -105,11 +100,7 @@ public class BackupCoreAPITest extends SolrTestCaseJ4 {
     backupCoreRequestBody.backupName = backupName;
 
     final SolrException solrException =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              backupCoreAPI.createBackup(null, backupCoreRequestBody);
-            });
+        expectThrows(SolrException.class, () -> api.createBackup(null, backupCoreRequestBody));
     assertEquals(400, solrException.code());
     assertTrue(
         "Exception message differed from expected: " + solrException.getMessage(),
@@ -125,9 +116,7 @@ public class BackupCoreAPITest extends SolrTestCaseJ4 {
     final SolrException solrException =
         expectThrows(
             SolrException.class,
-            () -> {
-              backupCoreAPI.createBackup("non-existent-core", backupCoreRequestBody);
-            });
+            () -> api.createBackup("non-existent-core", backupCoreRequestBody));
     assertEquals(500, solrException.code());
   }
 
@@ -138,7 +127,7 @@ public class BackupCoreAPITest extends SolrTestCaseJ4 {
     backupCoreRequestBody.shardBackupId = "md_shard1_0";
     IncrementalShardBackup.IncrementalShardSnapshotResponse response =
         (IncrementalShardBackup.IncrementalShardSnapshotResponse)
-            backupCoreAPI.createBackup(coreName, backupCoreRequestBody);
+            api.createBackup(coreName, backupCoreRequestBody);
 
     assertEquals(1, response.indexFileCount);
     assertEquals(1, response.uploadedIndexFileCount);
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/BalanceShardUniqueAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/BalanceShardUniqueAPITest.java
index c986f332da8..c9952dcca93 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/BalanceShardUniqueAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/BalanceShardUniqueAPITest.java
@@ -17,30 +17,36 @@
 
 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_ACTIVE_NODES;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.SHARD_UNIQUE;
 import static org.apache.solr.common.cloud.ZkStateReader.PROPERTY_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.BalanceShardUniqueRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link BalanceShardUnique} */
-public class BalanceShardUniqueAPITest extends SolrTestCaseJ4 {
+public class BalanceShardUniqueAPITest extends MockV2APITest {
+
+  private BalanceShardUnique api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new BalanceShardUnique(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
 
   @Test
   public void testReportsErrorIfRequestBodyMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new BalanceShardUnique(null, null, null);
-              api.balanceShardUnique("someCollectionName", null);
-            });
+        expectThrows(SolrException.class, () -> api.balanceShardUnique("someCollectionName", null));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required request body", thrown.getMessage());
@@ -51,12 +57,7 @@ public class BalanceShardUniqueAPITest extends SolrTestCaseJ4 {
     final var requestBody = new BalanceShardUniqueRequestBody();
     requestBody.property = "preferredLeader";
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new BalanceShardUnique(null, null, null);
-              api.balanceShardUnique(null, requestBody);
-            });
+        expectThrows(SolrException.class, () -> api.balanceShardUnique(null, requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
@@ -68,32 +69,31 @@ public class BalanceShardUniqueAPITest extends SolrTestCaseJ4 {
     final var requestBody = new BalanceShardUniqueRequestBody();
     final SolrException thrown =
         expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new BalanceShardUnique(null, null, null);
-              api.balanceShardUnique("someCollName", requestBody);
-            });
+            SolrException.class, () -> api.balanceShardUnique("someCollName", requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: property", thrown.getMessage());
   }
 
   @Test
-  public void testCreateRemoteMessageAllProperties() {
+  public void testCreateRemoteMessageAllProperties() throws Exception {
     final var requestBody = new BalanceShardUniqueRequestBody();
     requestBody.property = "someProperty";
     requestBody.shardUnique = Boolean.TRUE;
     requestBody.onlyActiveNodes = Boolean.TRUE;
     requestBody.async = "someAsyncId";
-    final var remoteMessage =
-        BalanceShardUnique.createRemoteMessage("someCollName", requestBody).getProperties();
-
-    assertEquals(6, remoteMessage.size());
-    assertEquals("balanceshardunique", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollName", remoteMessage.get(COLLECTION));
-    assertEquals("someProperty", remoteMessage.get(PROPERTY_PROP));
-    assertEquals(Boolean.TRUE, remoteMessage.get(SHARD_UNIQUE));
-    assertEquals(Boolean.TRUE, remoteMessage.get(ONLY_ACTIVE_NODES));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+
+    api.balanceShardUnique("someCollName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.BALANCESHARDUNIQUE,
+        requestBody.async,
+        message -> {
+          assertEquals(4, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals("someProperty", message.get(PROPERTY_PROP));
+          assertEquals(Boolean.TRUE, message.get(SHARD_UNIQUE));
+          assertEquals(Boolean.TRUE, message.get(ONLY_ACTIVE_NODES));
+        });
   }
 }
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
index 5ada271d324..12b72b2c563 100644
--- 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
@@ -34,7 +34,7 @@ import org.junit.Test;
 
 public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
 
-  private CoreSnapshot coreSnapshotAPI;
+  private CoreSnapshot api;
 
   @BeforeClass
   public static void initializeCoreAndRequestFactory() throws Exception {
@@ -54,7 +54,7 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
     CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker =
         new CoreAdminHandler.CoreAdminAsyncTracker();
 
-    coreSnapshotAPI =
+    api =
         new CoreSnapshot(solrQueryRequest, solrQueryResponse, coreContainer, coreAdminAsyncTracker);
   }
 
@@ -63,7 +63,7 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
   @After
   public void deleteSnapshots() throws Exception {
     for (String snapshotName : snapshotsToCleanup) {
-      coreSnapshotAPI.deleteSnapshot(coreName, snapshotName, null);
+      api.deleteSnapshot(coreName, snapshotName, null);
     }
 
     snapshotsToCleanup.clear();
@@ -73,8 +73,7 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
   public void testCreateSnapshotReturnsValidResponse() throws Exception {
     final String snapshotName = "my-new-snapshot";
 
-    final CreateCoreSnapshotResponse response =
-        coreSnapshotAPI.createSnapshot(coreName, snapshotName, null);
+    final CreateCoreSnapshotResponse response = api.createSnapshot(coreName, snapshotName, null);
     snapshotsToCleanup.add(snapshotName);
 
     assertEquals(coreName, response.core);
@@ -91,9 +90,7 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
     final SolrException solrException =
         expectThrows(
             SolrException.class,
-            () -> {
-              coreSnapshotAPI.createSnapshot(nonExistentCoreName, "my-new-snapshot", null);
-            });
+            () -> api.createSnapshot(nonExistentCoreName, "my-new-snapshot", null));
     assertEquals(400, solrException.code());
     assertTrue(
         "Exception message differed from expected: " + solrException.getMessage(),
@@ -106,11 +103,11 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
 
     for (int i = 0; i < 5; i++) {
       final String snapshotName = snapshotNameBase + i;
-      coreSnapshotAPI.createSnapshot(coreName, snapshotName, null);
+      api.createSnapshot(coreName, snapshotName, null);
       snapshotsToCleanup.add(snapshotName);
     }
 
-    final ListCoreSnapshotsResponse response = coreSnapshotAPI.listSnapshots(coreName);
+    final ListCoreSnapshotsResponse response = api.listSnapshots(coreName);
 
     assertEquals(5, response.snapshots.size());
   }
@@ -123,7 +120,7 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
         expectThrows(
             SolrException.class,
             () -> {
-              coreSnapshotAPI.listSnapshots(nonExistentCoreName);
+              api.listSnapshots(nonExistentCoreName);
             });
     assertEquals(400, solrException.code());
     assertTrue(
@@ -135,15 +132,14 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
   public void testDeleteSnapshotReturnsValidResponse() throws Exception {
     final String snapshotName = "my-new-snapshot";
 
-    coreSnapshotAPI.createSnapshot(coreName, snapshotName, null);
+    api.createSnapshot(coreName, snapshotName, null);
 
-    final DeleteSnapshotResponse deleteResponse =
-        coreSnapshotAPI.deleteSnapshot(coreName, snapshotName, null);
+    final DeleteSnapshotResponse deleteResponse = api.deleteSnapshot(coreName, snapshotName, null);
 
     assertEquals(coreName, deleteResponse.coreName);
     assertEquals(snapshotName, deleteResponse.commitName);
 
-    final ListCoreSnapshotsResponse response = coreSnapshotAPI.listSnapshots(coreName);
+    final ListCoreSnapshotsResponse response = api.listSnapshots(coreName);
 
     assertEquals(0, response.snapshots.size());
   }
@@ -155,9 +151,7 @@ public class CoreSnapshotAPITest extends SolrTestCaseJ4 {
     final SolrException solrException =
         expectThrows(
             SolrException.class,
-            () -> {
-              coreSnapshotAPI.deleteSnapshot(nonExistentCoreName, "non-existent-snapshot", null);
-            });
+            () -> api.deleteSnapshot(nonExistentCoreName, "non-existent-snapshot", null));
     assertEquals(400, solrException.code());
     assertTrue(
         "Exception message differed from expected: " + solrException.getMessage(),
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java
index b4f5e2d2760..d44152becaa 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java
@@ -18,33 +18,38 @@
 package org.apache.solr.handler.admin.api;
 
 import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.COLLECTIONS;
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.hamcrest.Matchers.containsString;
+import static org.mockito.Mockito.when;
 
 import java.util.List;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CategoryRoutedAliasProperties;
 import org.apache.solr.client.api.model.CreateAliasRequestBody;
 import org.apache.solr.client.api.model.CreateCollectionRequestBody;
 import org.apache.solr.client.api.model.RoutedAliasProperties;
 import org.apache.solr.client.api.model.TimeRoutedAliasProperties;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link CreateAlias} */
-public class CreateAliasAPITest extends SolrTestCaseJ4 {
+public class CreateAliasAPITest extends MockV2APITest {
+
+  private CreateAlias api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new CreateAlias(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
 
   @Test
   public void testReportsErrorIfRequestBodyMissing() {
-    final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateAlias(null, null, null);
-              api.createAlias(null);
-            });
+    final SolrException thrown = expectThrows(SolrException.class, () -> api.createAlias(null));
 
     assertEquals(400, thrown.code());
     assertEquals("Request body is required but missing", thrown.getMessage());
@@ -202,24 +207,26 @@ public class CreateAliasAPITest extends SolrTestCaseJ4 {
   }
 
   @Test
-  public void testRemoteMessageCreationForTraditionalAlias() {
+  public void testRemoteMessageCreationForTraditionalAlias() throws Exception {
     final var requestBody = new CreateAliasRequestBody();
     requestBody.name = "someAliasName";
     requestBody.collections = List.of("validColl1", "validColl2");
     requestBody.async = "someAsyncId";
 
-    final var remoteMessage =
-        CreateAlias.createRemoteMessageForTraditionalAlias(requestBody).getProperties();
+    api.createAlias(requestBody);
 
-    assertEquals(4, remoteMessage.size());
-    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someAliasName", remoteMessage.get("name"));
-    assertEquals("validColl1,validColl2", remoteMessage.get(COLLECTIONS));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATEALIAS,
+        requestBody.async,
+        message -> {
+          assertEquals(2, message.size());
+          assertEquals("someAliasName", message.get("name"));
+          assertEquals("validColl1,validColl2", message.get(COLLECTIONS));
+        });
   }
 
   @Test
-  public void testRemoteMessageCreationForCategoryRoutedAlias() {
+  public void testRemoteMessageCreationForCategoryRoutedAlias() throws Exception {
     final var requestBody = new CreateAliasRequestBody();
     requestBody.name = "someAliasName";
     final var categoryRouter = new CategoryRoutedAliasProperties();
@@ -230,25 +237,27 @@ public class CreateAliasAPITest extends SolrTestCaseJ4 {
     createParams.config = "someConfig";
     requestBody.collCreationParameters = createParams;
 
-    final var remoteMessage =
-        CreateAlias.createRemoteMessageForRoutedAlias(requestBody).getProperties();
-
-    assertEquals(6, remoteMessage.size());
-    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someAliasName", remoteMessage.get("name"));
-    assertEquals("category", remoteMessage.get("router.name"));
-    assertEquals("someField", remoteMessage.get("router.field"));
-    assertEquals(3, remoteMessage.get("create-collection.numShards"));
-    assertEquals("someConfig", remoteMessage.get("create-collection.collection.configName"));
+    api.createAlias(requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATEALIAS,
+        message -> {
+          assertEquals(5, message.size());
+          assertEquals("someAliasName", message.get("name"));
+          assertEquals("category", message.get("router.name"));
+          assertEquals("someField", message.get("router.field"));
+          assertEquals(3, message.get("create-collection.numShards"));
+          assertEquals("someConfig", message.get("create-collection.collection.configName"));
+        });
   }
 
   @Test
-  public void testRemoteMessageCreationForTimeRoutedAlias() {
+  public void testRemoteMessageCreationForTimeRoutedAlias() throws Exception {
     final var requestBody = new CreateAliasRequestBody();
     requestBody.name = "someAliasName";
     final var timeRouter = new TimeRoutedAliasProperties();
     timeRouter.field = "someField";
-    timeRouter.start = "NOW";
+    timeRouter.start = "NOW/HOUR";
     timeRouter.interval = "+1MONTH";
     timeRouter.maxFutureMs = 123456L;
     requestBody.routers = List.of(timeRouter);
@@ -257,28 +266,30 @@ public class CreateAliasAPITest extends SolrTestCaseJ4 {
     createParams.config = "someConfig";
     requestBody.collCreationParameters = createParams;
 
-    final var remoteMessage =
-        CreateAlias.createRemoteMessageForRoutedAlias(requestBody).getProperties();
-
-    assertEquals(9, remoteMessage.size());
-    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someAliasName", remoteMessage.get("name"));
-    assertEquals("time", remoteMessage.get("router.name"));
-    assertEquals("someField", remoteMessage.get("router.field"));
-    assertEquals("NOW", remoteMessage.get("router.start"));
-    assertEquals("+1MONTH", remoteMessage.get("router.interval"));
-    assertEquals(Long.valueOf(123456L), remoteMessage.get("router.maxFutureMs"));
-    assertEquals(3, remoteMessage.get("create-collection.numShards"));
-    assertEquals("someConfig", remoteMessage.get("create-collection.collection.configName"));
+    api.createAlias(requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATEALIAS,
+        message -> {
+          assertEquals(8, message.size());
+          assertEquals("someAliasName", message.get("name"));
+          assertEquals("time", message.get("router.name"));
+          assertEquals("someField", message.get("router.field"));
+          assertEquals("NOW/HOUR", message.get("router.start"));
+          assertEquals("+1MONTH", message.get("router.interval"));
+          assertEquals(Long.valueOf(123456L), message.get("router.maxFutureMs"));
+          assertEquals(3, message.get("create-collection.numShards"));
+          assertEquals("someConfig", message.get("create-collection.collection.configName"));
+        });
   }
 
   @Test
-  public void testRemoteMessageCreationForMultiDimensionalRoutedAlias() {
+  public void testRemoteMessageCreationForMultiDimensionalRoutedAlias() throws Exception {
     final var requestBody = new CreateAliasRequestBody();
     requestBody.name = "someAliasName";
     final var timeRouter = new TimeRoutedAliasProperties();
     timeRouter.field = "someField";
-    timeRouter.start = "NOW";
+    timeRouter.start = "NOW/HOUR";
     timeRouter.interval = "+1MONTH";
     timeRouter.maxFutureMs = 123456L;
     final var categoryRouter = new CategoryRoutedAliasProperties();
@@ -289,21 +300,23 @@ public class CreateAliasAPITest extends SolrTestCaseJ4 {
     createParams.config = "someConfig";
     requestBody.collCreationParameters = createParams;
 
-    final var remoteMessage =
-        CreateAlias.createRemoteMessageForRoutedAlias(requestBody).getProperties();
-
-    assertEquals(11, remoteMessage.size());
-    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someAliasName", remoteMessage.get("name"));
-    assertEquals("time", remoteMessage.get("router.0.name"));
-    assertEquals("someField", remoteMessage.get("router.0.field"));
-    assertEquals("NOW", remoteMessage.get("router.0.start"));
-    assertEquals("+1MONTH", remoteMessage.get("router.0.interval"));
-    assertEquals(Long.valueOf(123456L), remoteMessage.get("router.0.maxFutureMs"));
-    assertEquals("category", remoteMessage.get("router.1.name"));
-    assertEquals("someField", remoteMessage.get("router.1.field"));
-    assertEquals(3, remoteMessage.get("create-collection.numShards"));
-    assertEquals("someConfig", remoteMessage.get("create-collection.collection.configName"));
+    api.createAlias(requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATEALIAS,
+        message -> {
+          assertEquals(10, message.size());
+          assertEquals("someAliasName", message.get("name"));
+          assertEquals("time", message.get("router.0.name"));
+          assertEquals("someField", message.get("router.0.field"));
+          assertEquals("NOW/HOUR", message.get("router.0.start"));
+          assertEquals("+1MONTH", message.get("router.0.interval"));
+          assertEquals(Long.valueOf(123456L), message.get("router.0.maxFutureMs"));
+          assertEquals("category", message.get("router.1.name"));
+          assertEquals("someField", message.get("router.1.field"));
+          assertEquals(3, message.get("create-collection.numShards"));
+          assertEquals("someConfig", message.get("create-collection.collection.configName"));
+        });
   }
 
   private CreateAliasRequestBody requestBodyWithProvidedRouter(RoutedAliasProperties router) {
@@ -325,7 +338,7 @@ public class CreateAliasAPITest extends SolrTestCaseJ4 {
     v1Params.add("name", "someAliasName");
     v1Params.add("router.name", "Dimensional[time,category]");
     v1Params.add("router.0.field", "someField");
-    v1Params.add("router.0.start", "NOW");
+    v1Params.add("router.0.start", "NOW/HOUR");
     v1Params.add("router.0.interval", "+1MONTH");
     v1Params.add("router.0.maxFutureMs", "123456");
     v1Params.add("router.1.field", "someOtherField");
@@ -342,7 +355,7 @@ public class CreateAliasAPITest extends SolrTestCaseJ4 {
         requestBody.routers.get(0) instanceof TimeRoutedAliasProperties);
     final var timeRouter = (TimeRoutedAliasProperties) requestBody.routers.get(0);
     assertEquals("someField", timeRouter.field);
-    assertEquals("NOW", timeRouter.start);
+    assertEquals("NOW/HOUR", timeRouter.start);
     assertEquals("+1MONTH", timeRouter.interval);
     assertEquals(Long.valueOf(123456L), timeRouter.maxFutureMs);
     assertTrue(
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionAPITest.java
index 80a2200131a..55558803525 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionAPITest.java
@@ -17,7 +17,6 @@
 
 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.CREATE_NODE_SET;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.NUM_SLICES;
 import static org.apache.solr.common.cloud.DocCollection.CollectionStateProps.SHARDS;
@@ -30,31 +29,43 @@ import static org.apache.solr.common.params.CollectionAdminParams.PER_REPLICA_ST
 import static org.apache.solr.common.params.CollectionAdminParams.PULL_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.REPLICATION_FACTOR;
 import static org.apache.solr.common.params.CollectionAdminParams.TLOG_REPLICAS;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.apache.solr.common.params.CoreAdminParams.NAME;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
 
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CreateCollectionRequestBody;
 import org.apache.solr.client.api.model.CreateCollectionRouterProperties;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link CreateCollection}. */
-public class CreateCollectionAPITest extends SolrTestCaseJ4 {
+public class CreateCollectionAPITest extends MockV2APITest {
+
+  private CreateCollection api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new CreateCollection(mockCoreContainer, mockQueryRequest, queryResponse);
+    when(mockSolrZkClient.getData(eq("properties.json"), any(), any()))
+        .thenReturn("{}".getBytes(StandardCharsets.UTF_8));
+  }
 
   @Test
   public void testReportsErrorIfRequestBodyMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateCollection(null, null, null);
-              api.createCollection(null);
-            });
+        expectThrows(SolrException.class, () -> api.createCollection(null));
 
     assertEquals(400, thrown.code());
     assertEquals("Request body is missing but required", thrown.getMessage());
@@ -71,11 +82,7 @@ public class CreateCollectionAPITest extends SolrTestCaseJ4 {
     requestBody.nrtReplicas = 321;
 
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              CreateCollection.validateRequestBody(requestBody);
-            });
+        expectThrows(SolrException.class, () -> CreateCollection.validateRequestBody(requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals(
@@ -90,11 +97,7 @@ public class CreateCollectionAPITest extends SolrTestCaseJ4 {
     requestBody.config = "someConfig";
 
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              CreateCollection.validateRequestBody(requestBody);
-            });
+        expectThrows(SolrException.class, () -> CreateCollection.validateRequestBody(requestBody));
 
     assertEquals(400, thrown.code());
     assertTrue(
@@ -112,11 +115,7 @@ public class CreateCollectionAPITest extends SolrTestCaseJ4 {
     requestBody.shardNames = List.of("good-name", "bad;name");
 
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              CreateCollection.validateRequestBody(requestBody);
-            });
+        expectThrows(SolrException.class, () -> CreateCollection.validateRequestBody(requestBody));
 
     assertEquals(400, thrown.code());
     assertTrue(
@@ -125,7 +124,7 @@ public class CreateCollectionAPITest extends SolrTestCaseJ4 {
   }
 
   @Test
-  public void testCreateRemoteMessageAllProperties() {
+  public void testCreateRemoteMessageAllProperties() throws Exception {
     final var requestBody = new CreateCollectionRequestBody();
     requestBody.name = "someName";
     requestBody.replicationFactor = 123;
@@ -145,40 +144,49 @@ public class CreateCollectionAPITest extends SolrTestCaseJ4 {
     requestBody.nodeSet = List.of("node1", "node2");
     requestBody.shuffleNodes = false;
 
-    final var remoteMessage = CreateCollection.createRemoteMessage(requestBody).getProperties();
-
-    assertEquals("create", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("true", remoteMessage.get("fromApi"));
-    assertEquals("someName", remoteMessage.get(NAME));
-    assertEquals(123, remoteMessage.get(REPLICATION_FACTOR));
-    assertEquals("someConfig", remoteMessage.get(COLL_CONF));
-    assertEquals(456, remoteMessage.get(NUM_SLICES));
-    assertEquals("shard1,shard2", remoteMessage.get(SHARDS));
-    assertEquals(789, remoteMessage.get(PULL_REPLICAS));
-    assertEquals(987, remoteMessage.get(TLOG_REPLICAS));
-    assertEquals(123, remoteMessage.get(NRT_REPLICAS)); // replicationFactor value used
-    assertEquals(false, remoteMessage.get(WAIT_FOR_FINAL_STATE));
-    assertEquals(true, remoteMessage.get(PER_REPLICA_STATE));
-    assertEquals("someAliasName", remoteMessage.get(ALIAS));
-    assertEquals("propValue", remoteMessage.get("property.propName"));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
-    assertEquals("someRouterName", remoteMessage.get("router.name"));
-    assertEquals("someField", remoteMessage.get("router.field"));
-    assertEquals("node1,node2", remoteMessage.get(CREATE_NODE_SET));
-    assertEquals(false, remoteMessage.get(CREATE_NODE_SET_SHUFFLE_PARAM));
+    api.createCollection(requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATE,
+        requestBody.async,
+        message -> {
+          assertEquals(17, message.size());
+          assertEquals("true", message.get("fromApi"));
+          assertEquals("someName", message.get(NAME));
+          assertEquals(123, message.get(REPLICATION_FACTOR));
+          assertEquals("someConfig", message.get(COLL_CONF));
+          assertEquals(456, message.get(NUM_SLICES));
+          assertEquals("shard1,shard2", message.get(SHARDS));
+          assertEquals(789, message.get(PULL_REPLICAS));
+          assertEquals(987, message.get(TLOG_REPLICAS));
+          assertEquals(123, message.get(NRT_REPLICAS)); // replicationFactor value used
+          assertEquals(false, message.get(WAIT_FOR_FINAL_STATE));
+          assertEquals(true, message.get(PER_REPLICA_STATE));
+          assertEquals("someAliasName", message.get(ALIAS));
+          assertEquals("propValue", message.get("property.propName"));
+          assertEquals("someRouterName", message.get("router.name"));
+          assertEquals("someField", message.get("router.field"));
+          assertEquals("node1,node2", message.get(CREATE_NODE_SET));
+          assertEquals(false, message.get(CREATE_NODE_SET_SHUFFLE_PARAM));
+        });
   }
 
   @Test
-  public void testNoReplicaCreationMessage() {
+  public void testNoReplicaCreationMessage() throws Exception {
     final var requestBody = new CreateCollectionRequestBody();
     requestBody.name = "someName";
     requestBody.createReplicas = false;
+    requestBody.async = "someAsyncId";
 
-    final var remoteMessage = CreateCollection.createRemoteMessage(requestBody).getProperties();
+    api.createCollection(requestBody);
 
-    assertEquals("create", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someName", remoteMessage.get(NAME));
-    assertEquals("EMPTY", remoteMessage.get(CREATE_NODE_SET));
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATE,
+        requestBody.async,
+        message -> {
+          assertEquals("someName", message.get(NAME));
+          assertEquals("EMPTY", message.get(CREATE_NODE_SET));
+        });
   }
 
   @Test
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionSnapshotAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionSnapshotAPITest.java
index 27efa7bc0ef..c0046f293cf 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionSnapshotAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateCollectionSnapshotAPITest.java
@@ -16,48 +16,70 @@
  */
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.FOLLOW_ALIASES;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
 
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.client.api.model.CreateCollectionSnapshotRequestBody;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CoreAdminParams;
+import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mockito;
 
-public class CreateCollectionSnapshotAPITest extends SolrTestCaseJ4 {
+public class CreateCollectionSnapshotAPITest extends MockV2APITest {
+
+  private CreateCollectionSnapshot api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new CreateCollectionSnapshot(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
 
   @Test
-  public void testConstructsValidOverseerMessage() {
-    final ZkNodeProps messageOne =
-        CreateCollectionSnapshot.createRemoteMessage("myCollName", false, "mySnapshotName", null);
-    final Map<String, Object> rawMessageOne = messageOne.getProperties();
-    assertEquals(4, rawMessageOne.size());
-    assertThat(
-        rawMessageOne.keySet(),
-        containsInAnyOrder(
-            QUEUE_OPERATION, COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES));
-    assertEquals("createsnapshot", rawMessageOne.get(QUEUE_OPERATION));
-    assertEquals("myCollName", rawMessageOne.get(COLLECTION_PROP));
-    assertEquals("mySnapshotName", rawMessageOne.get(CoreAdminParams.COMMIT_NAME));
-    assertEquals(false, rawMessageOne.get(FOLLOW_ALIASES));
-
-    final ZkNodeProps messageTwo =
-        CreateCollectionSnapshot.createRemoteMessage(
-            "myCollName", true, "mySnapshotName", "myAsyncId");
-    final Map<String, Object> rawMessageTwo = messageTwo.getProperties();
-    assertEquals(5, rawMessageTwo.size());
-    assertThat(
-        rawMessageTwo.keySet(),
-        containsInAnyOrder(
-            QUEUE_OPERATION, COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES, ASYNC));
-    assertEquals("createsnapshot", rawMessageTwo.get(QUEUE_OPERATION));
-    assertEquals("myCollName", rawMessageTwo.get(COLLECTION_PROP));
-    assertEquals("mySnapshotName", rawMessageTwo.get(CoreAdminParams.COMMIT_NAME));
-    assertEquals(true, rawMessageTwo.get(FOLLOW_ALIASES));
-    assertEquals("myAsyncId", rawMessageTwo.get(ASYNC));
+  public void testConstructsValidOverseerMessage() throws Exception {
+    when(mockClusterState.hasCollection("myCollName")).thenReturn(true);
+    when(mockSolrZkClient.exists(anyString())).thenReturn(false);
+
+    CreateCollectionSnapshotRequestBody body = new CreateCollectionSnapshotRequestBody();
+    body.followAliases = false;
+
+    api.createCollectionSnapshot("myCollName", "mySnapshotName", body);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATESNAPSHOT,
+        message -> {
+          assertEquals(3, message.size());
+          assertThat(
+              message.keySet(),
+              containsInAnyOrder(COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES));
+          assertEquals("myCollName", message.get(COLLECTION_PROP));
+          assertEquals("mySnapshotName", message.get(CoreAdminParams.COMMIT_NAME));
+          assertEquals(false, message.get(FOLLOW_ALIASES));
+        });
+
+    body.followAliases = true;
+    body.async = "testId";
+    Mockito.clearInvocations(mockCommandRunner);
+    api.createCollectionSnapshot("myCollName", "mySnapshotName", body);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATESNAPSHOT,
+        body.async,
+        message -> {
+          assertEquals(3, message.size());
+          assertThat(
+              message.keySet(),
+              containsInAnyOrder(COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES));
+          assertEquals("myCollName", message.get(COLLECTION_PROP));
+          assertEquals("mySnapshotName", message.get(CoreAdminParams.COMMIT_NAME));
+          assertEquals(true, message.get(FOLLOW_ALIASES));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateReplicaAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateReplicaAPITest.java
index 2abe5d13397..83368ada741 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateReplicaAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateReplicaAPITest.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 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.REPLICA_TYPE;
@@ -35,26 +34,37 @@ import static org.apache.solr.common.params.CoreAdminParams.NAME;
 import static org.apache.solr.common.params.CoreAdminParams.NODE;
 import static org.apache.solr.common.params.CoreAdminParams.ULOG_DIR;
 import static org.apache.solr.common.params.ShardParams._ROUTE_;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
 
 import java.util.List;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CreateReplicaRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link CreateReplica} */
-public class CreateReplicaAPITest extends SolrTestCaseJ4 {
+public class CreateReplicaAPITest extends MockV2APITest {
+
+  private CreateReplica api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new CreateReplica(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfRequestBodyMissing() {
     final SolrException thrown =
         expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateReplica(null, null, null);
-              api.createReplica("someCollName", "someShardName", null);
-            });
+            SolrException.class, () -> api.createReplica("someCollName", "someShardName", null));
 
     assertEquals(400, thrown.code());
     assertEquals("Required request-body is missing", thrown.getMessage());
@@ -64,12 +74,7 @@ public class CreateReplicaAPITest extends SolrTestCaseJ4 {
   public void testReportsErrorIfCollectionNameMissing() {
     final var requestBody = new CreateReplicaRequestBody();
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateReplica(null, null, null);
-              api.createReplica(null, "shardName", requestBody);
-            });
+        expectThrows(SolrException.class, () -> api.createReplica(null, "shardName", requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
@@ -80,18 +85,14 @@ public class CreateReplicaAPITest extends SolrTestCaseJ4 {
     final var requestBody = new CreateReplicaRequestBody();
     final SolrException thrown =
         expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateReplica(null, null, null);
-              api.createReplica("someCollectionName", null, requestBody);
-            });
+            SolrException.class, () -> api.createReplica("someCollectionName", null, requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: shard", thrown.getMessage());
   }
 
   @Test
-  public void testCreateRemoteMessageAllProperties() {
+  public void testCreateRemoteMessageAllProperties() throws Exception {
     final var requestBody = new CreateReplicaRequestBody();
     requestBody.name = "someName";
     requestBody.type = "NRT";
@@ -110,31 +111,34 @@ public class CreateReplicaAPITest extends SolrTestCaseJ4 {
     requestBody.async = "someAsyncId";
     requestBody.properties = Map.of("propName1", "propVal1", "propName2", "propVal2");
 
-    final var remoteMessage =
-        CreateReplica.createRemoteMessage("someCollectionName", "someShardName", requestBody)
-            .getProperties();
-
-    assertEquals(20, remoteMessage.size());
-    assertEquals("addreplica", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollectionName", remoteMessage.get(COLLECTION));
-    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
-    assertEquals("someName", remoteMessage.get(NAME));
-    assertEquals("NRT", remoteMessage.get(REPLICA_TYPE));
-    assertEquals("/some/dir1", remoteMessage.get(INSTANCE_DIR));
-    assertEquals("/some/dir2", remoteMessage.get(DATA_DIR));
-    assertEquals("/some/dir3", remoteMessage.get(ULOG_DIR));
-    assertEquals("someRoute", remoteMessage.get(_ROUTE_));
-    assertEquals(123, remoteMessage.get(NRT_REPLICAS));
-    assertEquals(456, remoteMessage.get(TLOG_REPLICAS));
-    assertEquals(789, remoteMessage.get(PULL_REPLICAS));
-    assertEquals("node1,node2", remoteMessage.get(CREATE_NODE_SET_PARAM));
-    assertEquals("node3", remoteMessage.get(NODE));
-    assertEquals(Boolean.TRUE, remoteMessage.get(SKIP_NODE_ASSIGNMENT));
-    assertEquals(true, remoteMessage.get(WAIT_FOR_FINAL_STATE));
-    assertEquals(true, remoteMessage.get(FOLLOW_ALIASES));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
-    assertEquals("propVal1", remoteMessage.get("property.propName1"));
-    assertEquals("propVal2", remoteMessage.get("property.propName2"));
+    when(mockClusterState.hasCollection(eq("someCollectionName"))).thenReturn(true);
+
+    api.createReplica("someCollectionName", "someShardName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.ADDREPLICA,
+        requestBody.async,
+        message -> {
+          assertEquals(18, message.size());
+          assertEquals("someCollectionName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+          assertEquals("someName", message.get(NAME));
+          assertEquals("NRT", message.get(REPLICA_TYPE));
+          assertEquals("/some/dir1", message.get(INSTANCE_DIR));
+          assertEquals("/some/dir2", message.get(DATA_DIR));
+          assertEquals("/some/dir3", message.get(ULOG_DIR));
+          assertEquals("someRoute", message.get(_ROUTE_));
+          assertEquals(123, message.get(NRT_REPLICAS));
+          assertEquals(456, message.get(TLOG_REPLICAS));
+          assertEquals(789, message.get(PULL_REPLICAS));
+          assertEquals("node1,node2", message.get(CREATE_NODE_SET_PARAM));
+          assertEquals("node3", message.get(NODE));
+          assertEquals(Boolean.TRUE, message.get(SKIP_NODE_ASSIGNMENT));
+          assertEquals(true, message.get(WAIT_FOR_FINAL_STATE));
+          assertEquals(true, message.get(FOLLOW_ALIASES));
+          assertEquals("propVal1", message.get("property.propName1"));
+          assertEquals("propVal2", message.get("property.propName2"));
+        });
   }
 
   @Test
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
index a8bc3edf658..72051e96a11 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 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;
@@ -29,27 +28,40 @@ 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.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.hamcrest.Matchers.containsString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.util.List;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CreateShardRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.ImplicitDocRouter;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link CreateShard} */
-public class CreateShardAPITest extends SolrTestCaseJ4 {
+public class CreateShardAPITest extends MockV2APITest {
+
+  private CreateShard api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new CreateShard(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
 
   @Test
   public void testReportsErrorIfRequestBodyMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateShard(null, null, null);
-              api.createShard("someCollName", null);
-            });
+        expectThrows(SolrException.class, () -> api.createShard("someCollName", null));
 
     assertEquals(400, thrown.code());
     assertEquals("Required request-body is missing", thrown.getMessage());
@@ -60,12 +72,7 @@ public class CreateShardAPITest extends SolrTestCaseJ4 {
     final var requestBody = new CreateShardRequestBody();
     requestBody.shardName = "someShardName";
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateShard(null, null, null);
-              api.createShard(null, requestBody);
-            });
+        expectThrows(SolrException.class, () -> api.createShard(null, requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
@@ -76,12 +83,7 @@ public class CreateShardAPITest extends SolrTestCaseJ4 {
     final var requestBody = new CreateShardRequestBody();
     requestBody.shardName = null;
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateShard(null, null, null);
-              api.createShard("someCollectionName", requestBody);
-            });
+        expectThrows(SolrException.class, () -> api.createShard("someCollectionName", requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: shard", thrown.getMessage());
@@ -92,19 +94,14 @@ public class CreateShardAPITest extends SolrTestCaseJ4 {
     final var requestBody = new CreateShardRequestBody();
     requestBody.shardName = "invalid$shard@name";
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new CreateShard(null, null, null);
-              api.createShard("someCollectionName", requestBody);
-            });
+        expectThrows(SolrException.class, () -> api.createShard("someCollectionName", requestBody));
 
     assertEquals(400, thrown.code());
     assertThat(thrown.getMessage(), containsString("Invalid shard: [invalid$shard@name]"));
   }
 
   @Test
-  public void testCreateRemoteMessageAllProperties() {
+  public void testCreateRemoteMessageAllProperties() throws Exception {
     final var requestBody = new CreateShardRequestBody();
     requestBody.shardName = "someShardName";
     requestBody.replicationFactor = 123;
@@ -118,23 +115,31 @@ public class CreateShardAPITest extends SolrTestCaseJ4 {
     requestBody.async = "someAsyncId";
     requestBody.properties = Map.of("propName1", "propVal1", "propName2", "propVal2");
 
-    final var remoteMessage =
-        CreateShard.createRemoteMessage("someCollectionName", requestBody).getProperties();
-
-    assertEquals(13, remoteMessage.size());
-    assertEquals("createshard", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollectionName", remoteMessage.get(COLLECTION));
-    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
-    assertEquals(123, remoteMessage.get(REPLICATION_FACTOR));
-    assertEquals(123, remoteMessage.get(NRT_REPLICAS));
-    assertEquals(456, remoteMessage.get(TLOG_REPLICAS));
-    assertEquals(789, remoteMessage.get(PULL_REPLICAS));
-    assertEquals("node1,node2", remoteMessage.get(CREATE_NODE_SET_PARAM));
-    assertEquals(true, remoteMessage.get(WAIT_FOR_FINAL_STATE));
-    assertEquals(true, remoteMessage.get(FOLLOW_ALIASES));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
-    assertEquals("propVal1", remoteMessage.get("property.propName1"));
-    assertEquals("propVal2", remoteMessage.get("property.propName2"));
+    DocCollection mockCollection = mock(DocCollection.class);
+    when(mockClusterState.hasCollection(eq("someCollectionName"))).thenReturn(true);
+    when(mockClusterState.getCollection(eq("someCollectionName"))).thenReturn(mockCollection);
+    when(mockCollection.get(DocCollection.CollectionStateProps.DOC_ROUTER))
+        .thenReturn(Map.of(CommonParams.NAME, ImplicitDocRouter.NAME));
+
+    api.createShard("someCollectionName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.CREATESHARD,
+        requestBody.async,
+        message -> {
+          assertEquals(11, message.size());
+          assertEquals("someCollectionName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+          assertEquals(123, message.get(REPLICATION_FACTOR));
+          assertEquals(123, message.get(NRT_REPLICAS));
+          assertEquals(456, message.get(TLOG_REPLICAS));
+          assertEquals(789, message.get(PULL_REPLICAS));
+          assertEquals("node1,node2", message.get(CREATE_NODE_SET_PARAM));
+          assertEquals(true, message.get(WAIT_FOR_FINAL_STATE));
+          assertEquals(true, message.get(FOLLOW_ALIASES));
+          assertEquals("propVal1", message.get("property.propName1"));
+          assertEquals("propVal2", message.get("property.propName2"));
+        });
   }
 
   @Test
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java
index dc93c29b487..e2318e20f77 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java
@@ -17,28 +17,107 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-import static org.apache.solr.common.params.CoreAdminParams.NAME;
-
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
+import java.lang.invoke.MethodHandles;
+import java.time.Duration;
+import org.apache.solr.client.api.model.AsyncJerseyResponse;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.ZkClientClusterStateProvider;
+import org.apache.solr.client.solrj.request.AliasesApi;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.response.RequestStatusState;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Integration tests for {@link DeleteAlias} using the V2 API */
+public class DeleteAliasAPITest extends SolrCloudTestCase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1)
+        .addConfig(
+            "conf1", TEST_PATH().resolve("configsets").resolve("cloud-dynamic").resolve("conf"))
+        .configure();
+  }
+
+  @Test
+  public void testDeleteAlias() throws Exception {
+    CloudSolrClient cloudClient = cluster.getSolrClient();
+    String collectionName = "deletealiastest_coll";
+    String aliasName = "deletealiastest_alias";
+
+    // Create a collection
+    CollectionAdminRequest.createCollection(collectionName, "conf1", 1, 1).process(cloudClient);
+    cluster.waitForActiveCollection(collectionName, 1, 1);
+
+    // Create an alias pointing to the collection
+    CollectionAdminRequest.createAlias(aliasName, collectionName).process(cloudClient);
+
+    // Verify the alias exists
+    var clusterStateProvider = cloudClient.getClusterStateProvider();
+    assertEquals(
+        "Alias should exist before deletion",
+        collectionName,
+        clusterStateProvider.resolveSimpleAlias(aliasName));
 
-/** Unit tests for {@link DeleteAlias}. */
-public class DeleteAliasAPITest extends SolrTestCaseJ4 {
+    // Delete the alias using the V2 API
+    var request = new AliasesApi.DeleteAlias(aliasName);
+    AsyncJerseyResponse response = request.process(cloudClient);
+    assertNotNull(response);
+
+    // Verify the alias is gone
+    ZkStateReader.AliasesManager aliasesManager =
+        ((ZkClientClusterStateProvider) clusterStateProvider)
+            .getZkStateReader()
+            .getAliasesManager();
+    aliasesManager.update();
+    assertFalse(
+        "Alias should not exist after deletion", aliasesManager.getAliases().hasAlias(aliasName));
+  }
 
   @Test
-  public void testConstructsValidRemoteMessage() {
-    Map<String, Object> props = DeleteAlias.createRemoteMessage("aliasName", null).getProperties();
-    assertEquals(2, props.size());
-    assertEquals("deletealias", props.get(QUEUE_OPERATION));
-    assertEquals("aliasName", props.get(NAME));
-
-    props = DeleteAlias.createRemoteMessage("aliasName", "asyncId").getProperties();
-    assertEquals(3, props.size());
-    assertEquals("deletealias", props.get(QUEUE_OPERATION));
-    assertEquals("aliasName", props.get(NAME));
-    assertEquals("asyncId", props.get(ASYNC));
+  public void testDeleteAliasAsync() throws Exception {
+    CloudSolrClient cloudClient = cluster.getSolrClient();
+    String collectionName = "deletealiastest_coll_async";
+    String aliasName = "deletealiastest_alias_async";
+
+    // Create a collection
+    CollectionAdminRequest.createCollection(collectionName, "conf1", 1, 1).process(cloudClient);
+    cluster.waitForActiveCollection(collectionName, 1, 1);
+
+    // Create an alias pointing to the collection
+    CollectionAdminRequest.createAlias(aliasName, collectionName).process(cloudClient);
+
+    // Verify the alias exists
+    var clusterStateProvider = cloudClient.getClusterStateProvider();
+
+    // Delete the alias using the V2 API with async
+    String asyncId = "deleteAlias001";
+    var request = new AliasesApi.DeleteAlias(aliasName);
+    request.setAsync(asyncId);
+    var response = request.process(cloudClient);
+    assertNotNull(response);
+
+    // Wait for the async request to complete
+    CollectionAdminRequest.RequestStatusResponse rsp =
+        waitForAsyncClusterRequest(asyncId, Duration.ofSeconds(5));
+
+    assertEquals(
+        "Expected async request to complete successfully",
+        RequestStatusState.COMPLETED,
+        rsp.getRequestStatus());
+
+    // Verify the alias is gone
+    ZkStateReader.AliasesManager aliasesManager =
+        ((ZkClientClusterStateProvider) clusterStateProvider)
+            .getZkStateReader()
+            .getAliasesManager();
+    aliasesManager.update();
+    assertFalse(
+        "Alias should not exist after deletion", aliasesManager.getAliases().hasAlias(aliasName));
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
index fafc0a67c00..e126376d2a6 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
@@ -17,44 +17,53 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionParams.NAME;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
-import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.mockito.Mockito.when;
 
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
+import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 /** Unit tests for {@link DeleteCollection} */
-public class DeleteCollectionAPITest extends SolrTestCaseJ4 {
+public class DeleteCollectionAPITest extends MockV2APITest {
+
+  private DeleteCollection api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new DeleteCollection(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
 
   @Test
-  public void testConstructsValidOverseerMessage() {
+  public void testConstructsValidOverseerMessage() throws Exception {
     // Only required properties provided
-    {
-      final ZkNodeProps message = DeleteCollection.createRemoteMessage("someCollName", null, null);
-      final Map<String, Object> rawMessage = message.getProperties();
-      assertEquals(2, rawMessage.size());
-      assertThat(rawMessage.keySet(), containsInAnyOrder(QUEUE_OPERATION, NAME));
-      assertEquals("delete", rawMessage.get(QUEUE_OPERATION));
-      assertEquals("someCollName", rawMessage.get(NAME));
-    }
-
-    // Optional properties ('followAliases' and 'async') also provided
-    {
-      final ZkNodeProps message =
-          DeleteCollection.createRemoteMessage("someCollName", Boolean.TRUE, "someAsyncId");
-      final Map<String, Object> rawMessage = message.getProperties();
-      assertEquals(4, rawMessage.size());
-      assertThat(
-          rawMessage.keySet(), containsInAnyOrder(QUEUE_OPERATION, NAME, ASYNC, FOLLOW_ALIASES));
-      assertEquals("delete", rawMessage.get(QUEUE_OPERATION));
-      assertEquals("someCollName", rawMessage.get(NAME));
-      assertEquals(Boolean.TRUE, rawMessage.get(FOLLOW_ALIASES));
-      assertEquals("someAsyncId", rawMessage.get(ASYNC));
-    }
+    api.deleteCollection("someCollName", null, null);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETE,
+        message -> {
+          assertEquals(1, message.size());
+          assertThat(message, hasEntry(NAME, "someCollName"));
+        });
+
+    // Optional property 'followAliases' also provided
+    Mockito.clearInvocations(mockCommandRunner);
+    api.deleteCollection("someCollName", true, "test");
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETE,
+        "test",
+        message -> {
+          assertEquals(2, message.size());
+          assertThat(message, hasEntry(NAME, "someCollName"));
+          assertThat(message, hasEntry(FOLLOW_ALIASES, Boolean.TRUE));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionBackupAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionBackupAPITest.java
index 7d083d1c0cf..4053bed94f4 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionBackupAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionBackupAPITest.java
@@ -17,22 +17,48 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_ID;
 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.MAX_NUM_BACKUP_POINTS;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
+import java.net.URI;
 import org.apache.solr.client.api.model.PurgeUnusedFilesRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link DeleteCollectionBackup} */
-public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
+public class DeleteCollectionBackupAPITest extends MockV2APITest {
+
+  private DeleteCollectionBackup api;
+  private BackupRepository mockBackupRepository;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    mockBackupRepository = mock(BackupRepository.class);
+    when(mockCoreContainer.newBackupRepository(eq("someRepository")))
+        .thenReturn(mockBackupRepository);
+    when(mockCoreContainer.newBackupRepository(null)).thenReturn(mockBackupRepository);
+    when(mockBackupRepository.getBackupLocation(eq("someLocation"))).thenReturn("someLocation");
+    URI uri = new URI("someLocation");
+    when(mockBackupRepository.createDirectoryURI(eq("someLocation"))).thenReturn(uri);
+    when(mockBackupRepository.exists(eq(uri))).thenReturn(true);
+
+    api = new DeleteCollectionBackup(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfBackupNameMissing() {
     // Single delete
@@ -40,11 +66,9 @@ public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
       final SolrException thrown =
           expectThrows(
               SolrException.class,
-              () -> {
-                final var api = new DeleteCollectionBackup(null, null, null);
-                api.deleteSingleBackupById(
-                    null, "someBackupId", "someLocation", "someRepository", "someAsyncId");
-              });
+              () ->
+                  api.deleteSingleBackupById(
+                      null, "someBackupId", "someLocation", "someRepository", "someAsyncId"));
 
       assertEquals(400, thrown.code());
       assertEquals("Missing required parameter: name", thrown.getMessage());
@@ -55,11 +79,9 @@ public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
       final SolrException thrown =
           expectThrows(
               SolrException.class,
-              () -> {
-                final var api = new DeleteCollectionBackup(null, null, null);
-                api.deleteMultipleBackupsByRecency(
-                    null, 123, "someLocation", "someRepository", "someAsyncId");
-              });
+              () ->
+                  api.deleteMultipleBackupsByRecency(
+                      null, 123, "someLocation", "someRepository", "someAsyncId"));
 
       assertEquals(400, thrown.code());
       assertEquals("Missing required parameter: name", thrown.getMessage());
@@ -73,11 +95,7 @@ public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
       requestBody.async = "someAsyncId";
       final SolrException thrown =
           expectThrows(
-              SolrException.class,
-              () -> {
-                final var api = new DeleteCollectionBackup(null, null, null);
-                api.garbageCollectUnusedBackupFiles(null, requestBody);
-              });
+              SolrException.class, () -> api.garbageCollectUnusedBackupFiles(null, requestBody));
 
       assertEquals(400, thrown.code());
       assertEquals("Missing required parameter: name", thrown.getMessage());
@@ -89,11 +107,9 @@ public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteCollectionBackup(null, null, null);
-              api.deleteSingleBackupById(
-                  "someBackupName", null, "someLocation", "someRepository", "someAsyncId");
-            });
+            () ->
+                api.deleteSingleBackupById(
+                    "someBackupName", null, "someLocation", "someRepository", "someAsyncId"));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: backupId", thrown.getMessage());
@@ -104,11 +120,9 @@ public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteCollectionBackup(null, null, null);
-              api.deleteMultipleBackupsByRecency(
-                  "someBackupName", null, "someLocation", "someRepository", "someAsyncId");
-            });
+            () ->
+                api.deleteMultipleBackupsByRecency(
+                    "someBackupName", null, "someLocation", "someRepository", "someAsyncId"));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: retainLatest", thrown.getMessage());
@@ -117,39 +131,53 @@ public class DeleteCollectionBackupAPITest extends SolrTestCaseJ4 {
   // The message created in this test isn't valid in practice, since it contains mutually-exclusive
   // parameters, but that doesn't matter for the purposes of this test.
   @Test
-  public void testCreateRemoteMessageAllParams() {
-    final var remoteMessage =
-        DeleteCollectionBackup.createRemoteMessage(
-                "someBackupName",
-                "someBackupId",
-                123,
-                true,
-                "someLocation",
-                "someRepository",
-                "someAsyncId")
-            .getProperties();
-
-    assertEquals(8, remoteMessage.size());
-    assertEquals("deletebackup", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someBackupName", remoteMessage.get(NAME));
-    assertEquals("someBackupId", remoteMessage.get(BACKUP_ID));
-    assertEquals(Integer.valueOf(123), remoteMessage.get(MAX_NUM_BACKUP_POINTS));
-    assertEquals(Boolean.TRUE, remoteMessage.get(BACKUP_PURGE_UNUSED));
-    assertEquals("someLocation", remoteMessage.get(BACKUP_LOCATION));
-    assertEquals("someRepository", remoteMessage.get(BACKUP_REPOSITORY));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+  public void testCreateRemoteMessageSingle() throws Exception {
+    api.deleteSingleBackupById(
+        "someBackupName", "someBackupId", "someLocation", "someRepository", "someAsyncId");
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEBACKUP,
+        "someAsyncId",
+        message -> {
+          assertEquals(4, message.size());
+          assertEquals("someBackupName", message.get(NAME));
+          assertEquals("someBackupId", message.get(BACKUP_ID));
+          assertEquals("someLocation", message.get(BACKUP_LOCATION));
+          assertEquals("someRepository", message.get(BACKUP_REPOSITORY));
+        });
+  }
+
+  @Test
+  public void testCreateRemoteMessageMultiple() throws Exception {
+    api.deleteMultipleBackupsByRecency("someBackupName", 2, "someLocation", "someRepository", null);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEBACKUP,
+        message -> {
+          assertEquals(4, message.size());
+          assertEquals("someBackupName", message.get(NAME));
+          assertEquals(2, message.get(MAX_NUM_BACKUP_POINTS));
+          assertEquals("someLocation", message.get(BACKUP_LOCATION));
+          assertEquals("someRepository", message.get(BACKUP_REPOSITORY));
+        });
   }
 
   @Test
-  public void testCreateRemoteMessageOnlyRequiredParams() {
-    final var remoteMessage =
-        DeleteCollectionBackup.createRemoteMessage(
-                "someBackupName", "someBackupId", null, null, null, null, null)
-            .getProperties();
-
-    assertEquals(3, remoteMessage.size());
-    assertEquals("deletebackup", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someBackupName", remoteMessage.get(NAME));
-    assertEquals("someBackupId", remoteMessage.get(BACKUP_ID));
+  public void testCreateRemoteMessageGarbageCollect() throws Exception {
+    PurgeUnusedFilesRequestBody body = new PurgeUnusedFilesRequestBody();
+    body.location = "someLocation";
+    body.repositoryName = "someRepository";
+
+    api.garbageCollectUnusedBackupFiles("someBackupName", body);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEBACKUP,
+        message -> {
+          assertEquals(4, message.size());
+          assertEquals("someBackupName", message.get(NAME));
+          assertEquals(Boolean.TRUE, message.get(BACKUP_PURGE_UNUSED));
+          assertEquals("someLocation", message.get(BACKUP_LOCATION));
+          assertEquals("someRepository", message.get(BACKUP_REPOSITORY));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionSnapshotAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionSnapshotAPITest.java
index 0ed8e42c12c..13282e888bb 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionSnapshotAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionSnapshotAPITest.java
@@ -16,48 +16,64 @@
  */
 package org.apache.solr.handler.admin.api;
 
-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.params.CollectionAdminParams.FOLLOW_ALIASES;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
 
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CoreAdminParams;
+import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mockito;
 
-public class DeleteCollectionSnapshotAPITest extends SolrTestCaseJ4 {
+public class DeleteCollectionSnapshotAPITest extends MockV2APITest {
+
+  private DeleteCollectionSnapshot api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new DeleteCollectionSnapshot(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
 
   @Test
-  public void testConstructsValidOverseerMessage() {
-    final ZkNodeProps messageOne =
-        DeleteCollectionSnapshot.createRemoteMessage("myCollName", false, "mySnapshotName", null);
-    final Map<String, Object> rawMessageOne = messageOne.getProperties();
-    assertEquals(4, rawMessageOne.size());
-    assertThat(
-        rawMessageOne.keySet(),
-        containsInAnyOrder(
-            QUEUE_OPERATION, COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES));
-    assertEquals("deletesnapshot", rawMessageOne.get(QUEUE_OPERATION));
-    assertEquals("myCollName", rawMessageOne.get(COLLECTION_PROP));
-    assertEquals("mySnapshotName", rawMessageOne.get(CoreAdminParams.COMMIT_NAME));
-    assertEquals(false, rawMessageOne.get(FOLLOW_ALIASES));
-
-    final ZkNodeProps messageTwo =
-        DeleteCollectionSnapshot.createRemoteMessage(
-            "myCollName", true, "mySnapshotName", "myAsyncId");
-    final Map<String, Object> rawMessageTwo = messageTwo.getProperties();
-    assertEquals(5, rawMessageTwo.size());
-    assertThat(
-        rawMessageTwo.keySet(),
-        containsInAnyOrder(
-            QUEUE_OPERATION, COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES, ASYNC));
-    assertEquals("deletesnapshot", rawMessageTwo.get(QUEUE_OPERATION));
-    assertEquals("myCollName", rawMessageTwo.get(COLLECTION_PROP));
-    assertEquals("mySnapshotName", rawMessageTwo.get(CoreAdminParams.COMMIT_NAME));
-    assertEquals(true, rawMessageTwo.get(FOLLOW_ALIASES));
-    assertEquals("myAsyncId", rawMessageTwo.get(ASYNC));
+  public void testConstructsValidOverseerMessage() throws Exception {
+    when(mockClusterState.hasCollection("myCollName")).thenReturn(true);
+    when(mockSolrZkClient.exists(anyString())).thenReturn(false);
+
+    api.deleteCollectionSnapshot("myCollName", "mySnapshotName", false, null);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETESNAPSHOT,
+        message -> {
+          assertEquals(3, message.size());
+          assertThat(
+              message.keySet(),
+              containsInAnyOrder(COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES));
+          assertEquals("myCollName", message.get(COLLECTION_PROP));
+          assertEquals("mySnapshotName", message.get(CoreAdminParams.COMMIT_NAME));
+          assertEquals(false, message.get(FOLLOW_ALIASES));
+        });
+
+    Mockito.clearInvocations(mockCommandRunner);
+    api.deleteCollectionSnapshot("myCollName", "mySnapshotName", true, "testId");
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETESNAPSHOT,
+        "testId",
+        message -> {
+          assertEquals(3, message.size());
+          assertThat(
+              message.keySet(),
+              containsInAnyOrder(COLLECTION_PROP, CoreAdminParams.COMMIT_NAME, FOLLOW_ALIASES));
+          assertEquals("myCollName", message.get(COLLECTION_PROP));
+          assertEquals("mySnapshotName", message.get(CoreAdminParams.COMMIT_NAME));
+          assertEquals(true, message.get(FOLLOW_ALIASES));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java
index 74b6b3e927b..8339e632cde 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java
@@ -16,59 +16,171 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.mockito.Mockito.mock;
-
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.api.model.DeleteNodeRequestBody;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ZkNodeProps;
-import org.apache.solr.common.params.ModifiableSolrParams;
+import java.lang.invoke.MethodHandles;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.NodeApi;
+import org.apache.solr.client.solrj.response.RequestStatusState;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.Replica;
+import org.apache.solr.common.cloud.Slice;
+import org.apache.solr.common.util.StrUtils;
+import org.junit.After;
 import org.junit.BeforeClass;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-/** Unit tests for {@link DeleteNode} */
-public class DeleteNodeAPITest extends SolrTestCaseJ4 {
+/** Integration tests for {@link DeleteNode} using the V2 API */
+public class DeleteNodeAPITest extends SolrCloudTestCase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+  public static void setupCluster() throws Exception {
+    configureCluster(6)
+        .addConfig(
+            "conf1", TEST_PATH().resolve("configsets").resolve("cloud-dynamic").resolve("conf"))
+        .configure();
   }
 
-  @Test
-  public void testV1InvocationThrowsErrorsIfRequiredParametersMissing() {
-    final var api = mock(DeleteNode.class);
-    final SolrException e =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              DeleteNode.invokeUsingV1Inputs(api, new ModifiableSolrParams());
-            });
-    assertEquals("Missing required parameter: node", e.getMessage());
+  @After
+  public void clearCollections() throws Exception {
+    cluster.deleteAllCollections();
   }
 
   @Test
-  public void testValidOverseerMessageIsCreated() {
-    final var requestBody = new DeleteNodeRequestBody();
-    requestBody.async = "async";
-    final ZkNodeProps createdMessage =
-        DeleteNode.createRemoteMessage("nodeNameToDelete", requestBody);
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(3, createdMessageProps.size());
-    assertEquals("nodeNameToDelete", createdMessageProps.get("node"));
-    assertEquals("async", createdMessageProps.get("async"));
-    assertEquals("deletenode", createdMessageProps.get("operation"));
+  public void testDeleteNode() throws Exception {
+    CloudSolrClient cloudClient = cluster.getSolrClient();
+    String coll = "deletenodetest_coll";
+    Set<String> liveNodes = cloudClient.getClusterStateProvider().getLiveNodes();
+    ArrayList<String> l = new ArrayList<>(liveNodes);
+    Collections.shuffle(l, random());
+    CollectionAdminRequest.Create create =
+        pickRandom(
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 2, 0, 0),
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 1, 1, 0),
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 0, 1, 1),
+            // check RF=1
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 1, 0, 0),
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 0, 1, 0));
+    create.setCreateNodeSet(StrUtils.join(l, ','));
+    cloudClient.request(create);
+    String nodeToBeDecommissioned = l.get(0);
+
+    // check what replicas are on the node, and whether the call should fail
+    boolean shouldFail = false;
+    DocCollection docColl = cloudClient.getClusterStateProvider().getCollection(coll);
+    log.info("#### DocCollection: {}", docColl);
+    List<Replica> replicas = docColl.getReplicasOnNode(nodeToBeDecommissioned);
+    for (Replica replica : replicas) {
+      String shard = replica.getShard();
+      Slice slice = docColl.getSlice(shard);
+      boolean hasOtherNonPullReplicas = false;
+      for (Replica r : slice.getReplicas()) {
+        if (!r.getName().equals(replica.getName())
+            && !r.getNodeName().equals(nodeToBeDecommissioned)
+            && r.getType().leaderEligible) {
+          hasOtherNonPullReplicas = true;
+          break;
+        }
+      }
+      if (!hasOtherNonPullReplicas) {
+        shouldFail = true;
+        break;
+      }
+    }
+
+    var request = new NodeApi.DeleteNode(nodeToBeDecommissioned);
+    SubResponseAccumulatingJerseyResponse response = request.process(cloudClient);
+
+    if (log.isInfoEnabled()) {
+      log.info(
+          "####### DocCollection after: {}",
+          cloudClient.getClusterStateProvider().getClusterState().getCollection(coll));
+    }
+
+    if (shouldFail) {
+      assertNotNull(
+          "Expected request to fail, there should be failures sent back",
+          response.failedSubResponsesByNodeName);
+    } else {
+      assertNull(
+          "Expected request to not fail, there should be no failures sent back",
+          response.failedSubResponsesByNodeName);
+    }
   }
 
   @Test
-  public void testRequestBodyCanBeOmitted() throws Exception {
-    final ZkNodeProps createdMessage = DeleteNode.createRemoteMessage("nodeNameToDelete", null);
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(2, createdMessageProps.size());
-    assertEquals("nodeNameToDelete", createdMessageProps.get("node"));
-    assertEquals("deletenode", createdMessageProps.get("operation"));
-    assertFalse(
-        "Expected message to not contain value for async: " + createdMessageProps.get("async"),
-        createdMessageProps.containsKey("async"));
+  public void testDeleteNodeAsync() throws Exception {
+    CloudSolrClient cloudClient = cluster.getSolrClient();
+    String coll = "deletenodetest_coll_async";
+    Set<String> liveNodes = cloudClient.getClusterStateProvider().getLiveNodes();
+    ArrayList<String> l = new ArrayList<>(liveNodes);
+    Collections.shuffle(l, random());
+    CollectionAdminRequest.Create create =
+        pickRandom(
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 2, 0, 0),
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 1, 1, 0),
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 0, 1, 1),
+            // check RF=1
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 1, 0, 0),
+            CollectionAdminRequest.createCollection(coll, "conf1", 5, 0, 1, 0));
+    create.setCreateNodeSet(StrUtils.join(l, ','));
+    cloudClient.request(create);
+    String nodeToBeDecommissioned = l.get(0);
+
+    // check what replicas are on the node, and whether the call should fail
+    boolean shouldFail = false;
+    DocCollection docColl = cloudClient.getClusterStateProvider().getCollection(coll);
+    log.info("#### DocCollection: {}", docColl);
+    List<Replica> replicas = docColl.getReplicasOnNode(nodeToBeDecommissioned);
+    for (Replica replica : replicas) {
+      String shard = replica.getShard();
+      Slice slice = docColl.getSlice(shard);
+      boolean hasOtherNonPullReplicas = false;
+      for (Replica r : slice.getReplicas()) {
+        if (!r.getName().equals(replica.getName())
+            && !r.getNodeName().equals(nodeToBeDecommissioned)
+            && r.getType().leaderEligible) {
+          hasOtherNonPullReplicas = true;
+          break;
+        }
+      }
+      if (!hasOtherNonPullReplicas) {
+        shouldFail = true;
+        break;
+      }
+    }
+
+    String asyncId = "003";
+    var request = new NodeApi.DeleteNode(nodeToBeDecommissioned);
+    request.setAsync(asyncId);
+    SubResponseAccumulatingJerseyResponse response = request.process(cloudClient);
+    assertNull(
+        "Expected request to not fail, any failures will be returned in the async status response",
+        response.failedSubResponsesByNodeName);
+
+    // Wait for the async request to complete
+    CollectionAdminRequest.RequestStatusResponse rsp =
+        waitForAsyncClusterRequest(asyncId, Duration.ofSeconds(5));
+
+    if (log.isInfoEnabled()) {
+      log.info(
+          "####### DocCollection after: {}",
+          cloudClient.getClusterStateProvider().getClusterState().getCollection(coll));
+    }
+
+    if (shouldFail) {
+      assertSame(String.valueOf(rsp), RequestStatusState.FAILED, rsp.getRequestStatus());
+    } else {
+      assertNotSame(String.valueOf(rsp), RequestStatusState.FAILED, rsp.getRequestStatus());
+    }
   }
 }
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
index d0dca0bce8e..5ad1cfdbcad 100644
--- 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
@@ -17,34 +17,45 @@
 
 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 static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.api.model.ScaleCollectionRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link DeleteReplica} */
-public class DeleteReplicaAPITest extends SolrTestCaseJ4 {
+public class DeleteReplicaAPITest extends MockV2APITest {
+
+  private DeleteReplica api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new DeleteReplica(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteReplica(null, null, null);
-              api.deleteReplicaByName(
-                  null, "someShard", "someReplica", null, null, null, 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());
@@ -55,11 +66,9 @@ public class DeleteReplicaAPITest extends SolrTestCaseJ4 {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteReplica(null, null, null);
-              api.deleteReplicaByName(
-                  "someCollection", null, "someReplica", null, null, null, 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());
@@ -70,66 +79,100 @@ public class DeleteReplicaAPITest extends SolrTestCaseJ4 {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteReplica(null, null, null);
-              api.deleteReplicaByName(
-                  "someCollection", "someShard", null, null, null, null, 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 =
-        DeleteReplica.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));
+  public void testCreateRemoteMessageByName() throws Exception {
+    api.deleteReplicaByName(
+        "someCollName",
+        "someShardName",
+        "someReplicaName",
+        true,
+        false,
+        true,
+        false,
+        true,
+        "someAsyncId");
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEREPLICA,
+        "someAsyncId",
+        message -> {
+          assertEquals(8, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+          assertEquals("someReplicaName", message.get(REPLICA));
+          assertEquals(Boolean.TRUE, message.get(FOLLOW_ALIASES));
+          assertEquals(Boolean.FALSE, message.get(DELETE_INSTANCE_DIR));
+          assertEquals(Boolean.TRUE, message.get(DELETE_DATA_DIR));
+          assertEquals(Boolean.FALSE, message.get(DELETE_INDEX));
+          assertEquals(Boolean.TRUE, message.get(ONLY_IF_DOWN));
+        });
+  }
+
+  @Test
+  public void testCreateRemoteMessageByCount() throws Exception {
+    api.deleteReplicasByCount(
+        "someCollName", "someShardName", 123, true, false, true, false, true, "someAsyncId");
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEREPLICA,
+        "someAsyncId",
+        message -> {
+          assertEquals(8, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+          assertEquals(123, message.get(COUNT_PROP));
+          assertEquals(Boolean.TRUE, message.get(FOLLOW_ALIASES));
+          assertEquals(Boolean.FALSE, message.get(DELETE_INSTANCE_DIR));
+          assertEquals(Boolean.TRUE, message.get(DELETE_DATA_DIR));
+          assertEquals(Boolean.FALSE, message.get(DELETE_INDEX));
+          assertEquals(Boolean.TRUE, message.get(ONLY_IF_DOWN));
+        });
+  }
+
+  @Test
+  public void testCreateRemoteMessageByCountAllShards() throws Exception {
+    ScaleCollectionRequestBody body = new ScaleCollectionRequestBody();
+    body.numToDelete = 123;
+    body.followAliases = true;
+    body.deleteIndex = true;
+    body.onlyIfDown = false;
+    body.async = "someAsyncId";
+
+    api.deleteReplicasByCountAllShards("someCollName", body);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEREPLICA,
+        "someAsyncId",
+        message -> {
+          assertEquals(5, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals(123, message.get(COUNT_PROP));
+          assertEquals(Boolean.TRUE, message.get(FOLLOW_ALIASES));
+          assertEquals(Boolean.TRUE, message.get(DELETE_INDEX));
+          assertEquals(Boolean.FALSE, message.get(ONLY_IF_DOWN));
+        });
   }
 
   @Test
-  public void testMissingValuesExcludedFromRemoteMessage() {
-    final var remoteMessage =
-        DeleteReplica.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));
+  public void testMissingValuesExcludedFromRemoteMessage() throws Exception {
+    api.deleteReplicaByName(
+        "someCollName", "someShardName", "someReplicaName", null, null, null, null, null, null);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEREPLICA,
+        message -> {
+          assertEquals(3, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+          assertEquals("someReplicaName", message.get(REPLICA));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPITest.java
index 19c85b85481..3e7c487863e 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteReplicaPropertyAPITest.java
@@ -17,20 +17,18 @@
 
 package org.apache.solr.handler.admin.api;
 
-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.PROPERTY_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.REPLICA_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
-import org.junit.BeforeClass;
+import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -40,16 +38,21 @@ import org.junit.Test;
  * here focus primarily on how the v1 code invokes the v2 API and how the v2 API crafts its
  * overseer/Distributed State Processing RPC message.
  */
-public class DeleteReplicaPropertyAPITest extends SolrTestCaseJ4 {
+public class DeleteReplicaPropertyAPITest extends MockV2APITest {
 
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+  private DeleteReplicaProperty api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new DeleteReplicaProperty(mockCoreContainer, mockQueryRequest, queryResponse);
   }
 
   @Test
   public void testV1InvocationThrowsErrorsIfRequiredParametersMissing() {
-    final var api = mock(DeleteReplicaProperty.class);
     final var allParams = new ModifiableSolrParams();
     allParams.add(COLLECTION_PROP, "someColl");
     allParams.add(SHARD_ID_PROP, "someShard");
@@ -62,9 +65,7 @@ public class DeleteReplicaPropertyAPITest extends SolrTestCaseJ4 {
       final SolrException e =
           expectThrows(
               SolrException.class,
-              () -> {
-                DeleteReplicaProperty.invokeUsingV1Inputs(api, noCollectionParams);
-              });
+              () -> DeleteReplicaProperty.invokeUsingV1Inputs(api, noCollectionParams));
       assertEquals("Missing required parameter: " + COLLECTION_PROP, e.getMessage());
     }
 
@@ -74,9 +75,7 @@ public class DeleteReplicaPropertyAPITest extends SolrTestCaseJ4 {
       final SolrException e =
           expectThrows(
               SolrException.class,
-              () -> {
-                DeleteReplicaProperty.invokeUsingV1Inputs(api, noShardParams);
-              });
+              () -> DeleteReplicaProperty.invokeUsingV1Inputs(api, noShardParams));
       assertEquals("Missing required parameter: " + SHARD_ID_PROP, e.getMessage());
     }
 
@@ -98,53 +97,51 @@ public class DeleteReplicaPropertyAPITest extends SolrTestCaseJ4 {
       final SolrException e =
           expectThrows(
               SolrException.class,
-              () -> {
-                DeleteReplicaProperty.invokeUsingV1Inputs(api, noPropertyParams);
-              });
+              () -> DeleteReplicaProperty.invokeUsingV1Inputs(api, noPropertyParams));
       assertEquals("Missing required parameter: " + PROPERTY_PROP, e.getMessage());
     }
   }
 
   @Test
   public void testV1InvocationPassesAllProvidedParameters() throws Exception {
-    final var api = mock(DeleteReplicaProperty.class);
+    final var mockApi = mock(DeleteReplicaProperty.class);
     final var allParams = new ModifiableSolrParams();
     allParams.add(COLLECTION_PROP, "someColl");
     allParams.add(SHARD_ID_PROP, "someShard");
     allParams.add(REPLICA_PROP, "someReplica");
     allParams.add(PROPERTY_PROP, "somePropName");
 
-    DeleteReplicaProperty.invokeUsingV1Inputs(api, allParams);
+    DeleteReplicaProperty.invokeUsingV1Inputs(mockApi, allParams);
 
-    verify(api).deleteReplicaProperty("someColl", "someShard", "someReplica", "somePropName");
+    verify(mockApi).deleteReplicaProperty("someColl", "someShard", "someReplica", "somePropName");
   }
 
   @Test
   public void testV1InvocationTrimsPropertyNamePrefixIfProvided() throws Exception {
-    final var api = mock(DeleteReplicaProperty.class);
+    final var mockApi = mock(DeleteReplicaProperty.class);
     final var allParams = new ModifiableSolrParams();
     allParams.add(COLLECTION_PROP, "someColl");
     allParams.add(SHARD_ID_PROP, "someShard");
     allParams.add(REPLICA_PROP, "someReplica");
     allParams.add(PROPERTY_PROP, "property.somePropName"); // NOTE THE "property." prefix!
 
-    DeleteReplicaProperty.invokeUsingV1Inputs(api, allParams);
+    DeleteReplicaProperty.invokeUsingV1Inputs(mockApi, allParams);
 
-    verify(api).deleteReplicaProperty("someColl", "someShard", "someReplica", "somePropName");
+    verify(mockApi).deleteReplicaProperty("someColl", "someShard", "someReplica", "somePropName");
   }
 
   @Test
-  public void testRPCMessageCreation() {
-    final ZkNodeProps message =
-        DeleteReplicaProperty.createRemoteMessage(
-            "someColl", "someShard", "someReplica", "somePropName");
-    final Map<String, Object> props = message.getProperties();
-
-    assertEquals(5, props.size());
-    assertEquals("deletereplicaprop", props.get(QUEUE_OPERATION));
-    assertEquals("someColl", props.get(COLLECTION_PROP));
-    assertEquals("someShard", props.get(SHARD_ID_PROP));
-    assertEquals("someReplica", props.get(REPLICA_PROP));
-    assertEquals("somePropName", props.get(PROPERTY_PROP));
+  public void testRPCMessageCreation() throws Exception {
+    api.deleteReplicaProperty("someColl", "someShard", "someReplica", "somePropName");
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETEREPLICAPROP,
+        message -> {
+          assertEquals(4, message.size());
+          assertEquals("someColl", message.get(COLLECTION_PROP));
+          assertEquals("someShard", message.get(SHARD_ID_PROP));
+          assertEquals("someReplica", message.get(REPLICA_PROP));
+          assertEquals("somePropName", message.get(PROPERTY_PROP));
+        });
   }
 }
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
index e26653e6d89..6d5c4b73143 100644
--- 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
@@ -17,30 +17,40 @@
 
 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 static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link DeleteShard} */
-public class DeleteShardAPITest extends SolrTestCaseJ4 {
+public class DeleteShardAPITest extends MockV2APITest {
+
+  private DeleteShard api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new DeleteShard(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteShard(null, null, null);
-              api.deleteShard(null, "someShard", null, null, null, null, null);
-            });
+            () -> api.deleteShard(null, "someShard", null, null, null, null, null));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
@@ -51,43 +61,42 @@ public class DeleteShardAPITest extends SolrTestCaseJ4 {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new DeleteShard(null, null, null);
-              api.deleteShard("someCollection", null, null, null, 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 =
-        DeleteShard.createRemoteMessage(
-                "someCollName", "someShardName", true, false, true, false, "someAsyncId")
-            .getProperties();
+  public void testCreateRemoteMessageAllProperties() throws Exception {
+    when(mockClusterState.hasCollection(eq("someCollectionName"))).thenReturn(true);
+    api.deleteShard("someCollName", "someShardName", true, false, true, false, "someAsyncId");
 
-    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));
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETESHARD,
+        "someAsyncId",
+        message -> {
+          assertEquals(6, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+          assertEquals(Boolean.TRUE, message.get(DELETE_INSTANCE_DIR));
+          assertEquals(Boolean.FALSE, message.get(DELETE_DATA_DIR));
+          assertEquals(Boolean.TRUE, message.get(DELETE_INDEX));
+          assertEquals(Boolean.FALSE, message.get(FOLLOW_ALIASES));
+        });
   }
 
   @Test
-  public void testMissingValuesExcludedFromRemoteMessage() {
-    final var remoteMessage =
-        DeleteShard.createRemoteMessage(
-                "someCollName", "someShardName", null, null, null, null, null)
-            .getProperties();
+  public void testMissingValuesExcludedFromRemoteMessage() throws Exception {
+    when(mockClusterState.hasCollection(eq("someCollectionName"))).thenReturn(true);
+    api.deleteShard("someCollName", "someShardName", null, null, null, null, null);
 
-    assertEquals(3, remoteMessage.size());
-    assertEquals("deleteshard", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollName", remoteMessage.get(COLLECTION));
-    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+    validateRunCommand(
+        CollectionParams.CollectionAction.DELETESHARD,
+        message -> {
+          assertEquals(2, message.size());
+          assertEquals("someCollName", message.get(COLLECTION));
+          assertEquals("someShardName", message.get(SHARD_ID_PROP));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java
index be1fce44fb8..d93fc80af42 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java
@@ -17,21 +17,30 @@
 
 package org.apache.solr.handler.admin.api;
 
-import org.apache.solr.SolrTestCaseJ4;
+import static org.mockito.Mockito.when;
+
 import org.apache.solr.common.SolrException;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link ForceLeader} */
-public class ForceLeaderAPITest extends SolrTestCaseJ4 {
+public class ForceLeaderAPITest extends MockV2APITest {
+
+  private ForceLeader api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new ForceLeader(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new ForceLeader(null, null, null);
-              api.forceShardLeader(null, "someShard");
-            });
+        expectThrows(SolrException.class, () -> api.forceShardLeader(null, "someShard"));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
@@ -40,12 +49,7 @@ public class ForceLeaderAPITest extends SolrTestCaseJ4 {
   @Test
   public void testReportsErrorIfShardNameMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new ForceLeader(null, null, null);
-              api.forceShardLeader("someCollection", null);
-            });
+        expectThrows(SolrException.class, () -> api.forceShardLeader("someCollection", null));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: shard", thrown.getMessage());
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java
index 85f0ffc1d3c..0b9bbc04526 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java
@@ -16,66 +16,28 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.Collections;
-import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.MigrateReplicasRequestBody;
-import org.apache.solr.cloud.OverseerSolrResponse;
-import org.apache.solr.cloud.ZkController;
-import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.ZkNodeProps;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.core.CoreContainer;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.common.params.CollectionParams;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /** Unit tests for {@link ReplaceNode} */
-public class MigrateReplicasAPITest extends SolrTestCaseJ4 {
+public class MigrateReplicasAPITest extends MockV2APITest {
 
-  private CoreContainer mockCoreContainer;
-  private ZkController mockZkController;
-  private SolrQueryRequest mockQueryRequest;
-  private SolrQueryResponse queryResponse;
-  private MigrateReplicas migrateReplicasAPI;
-  private DistributedCollectionConfigSetCommandRunner mockCommandRunner;
-  private ArgumentCaptor<ZkNodeProps> messageCapturer;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
-  }
+  private MigrateReplicas api;
 
   @Override
   @Before
   public void setUp() throws Exception {
     super.setUp();
-
-    mockCoreContainer = mock(CoreContainer.class);
-    mockZkController = mock(ZkController.class);
-    mockCommandRunner = mock(DistributedCollectionConfigSetCommandRunner.class);
-    when(mockCoreContainer.getZkController()).thenReturn(mockZkController);
-    when(mockZkController.getDistributedCommandRunner()).thenReturn(Optional.of(mockCommandRunner));
-    when(mockCommandRunner.runCollectionCommand(any(), any(), anyLong()))
-        .thenReturn(new OverseerSolrResponse(new NamedList<>()));
-    mockQueryRequest = mock(SolrQueryRequest.class);
-    queryResponse = new SolrQueryResponse();
-    migrateReplicasAPI = new MigrateReplicas(mockCoreContainer, mockQueryRequest, queryResponse);
-    messageCapturer = ArgumentCaptor.forClass(ZkNodeProps.class);
-
     when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new MigrateReplicas(mockCoreContainer, mockQueryRequest, queryResponse);
   }
 
   @Test
@@ -83,31 +45,33 @@ public class MigrateReplicasAPITest extends SolrTestCaseJ4 {
     MigrateReplicasRequestBody requestBody =
         new MigrateReplicasRequestBody(
             Set.of("demoSourceNode"), Set.of("demoTargetNode"), false, "async");
-    migrateReplicasAPI.migrateReplicas(requestBody);
-    verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
 
-    final ZkNodeProps createdMessage = messageCapturer.getValue();
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(5, createdMessageProps.size());
-    assertEquals(Set.of("demoSourceNode"), createdMessageProps.get("sourceNodes"));
-    assertEquals(Set.of("demoTargetNode"), createdMessageProps.get("targetNodes"));
-    assertEquals(false, createdMessageProps.get("waitForFinalState"));
-    assertEquals("async", createdMessageProps.get("async"));
-    assertEquals("migrate_replicas", createdMessageProps.get("operation"));
+    api.migrateReplicas(requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.MIGRATE_REPLICAS,
+        "async",
+        message -> {
+          assertEquals(3, message.size());
+          assertEquals(Set.of("demoSourceNode"), message.get("sourceNodes"));
+          assertEquals(Set.of("demoTargetNode"), message.get("targetNodes"));
+          assertEquals(false, message.get("waitForFinalState"));
+        });
   }
 
   @Test
   public void testNoTargetNodes() throws Exception {
     MigrateReplicasRequestBody requestBody =
         new MigrateReplicasRequestBody(Set.of("demoSourceNode"), null, null, null);
-    migrateReplicasAPI.migrateReplicas(requestBody);
-    verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
 
-    final ZkNodeProps createdMessage = messageCapturer.getValue();
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(2, createdMessageProps.size());
-    assertEquals(Set.of("demoSourceNode"), createdMessageProps.get("sourceNodes"));
-    assertEquals("migrate_replicas", createdMessageProps.get("operation"));
+    api.migrateReplicas(requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.MIGRATE_REPLICAS,
+        message -> {
+          assertEquals(1, message.size());
+          assertEquals(Set.of("demoSourceNode"), message.get("sourceNodes"));
+        });
   }
 
   @Test
@@ -115,9 +79,9 @@ public class MigrateReplicasAPITest extends SolrTestCaseJ4 {
     MigrateReplicasRequestBody requestBody1 =
         new MigrateReplicasRequestBody(
             Collections.emptySet(), Set.of("demoTargetNode"), null, null);
-    assertThrows(SolrException.class, () -> migrateReplicasAPI.migrateReplicas(requestBody1));
+    assertThrows(SolrException.class, () -> api.migrateReplicas(requestBody1));
     MigrateReplicasRequestBody requestBody2 =
         new MigrateReplicasRequestBody(null, Set.of("demoTargetNode"), null, null);
-    assertThrows(SolrException.class, () -> migrateReplicasAPI.migrateReplicas(requestBody2));
+    assertThrows(SolrException.class, () -> api.migrateReplicas(requestBody2));
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/MockV2APITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/MockV2APITest.java
new file mode 100644
index 00000000000..1a660402963
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/MockV2APITest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.opentelemetry.api.trace.Span;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.cloud.OverseerSolrResponse;
+import org.apache.solr.cloud.ZkController;
+import org.apache.solr.cloud.api.collections.AdminCmdContext;
+import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
+import org.apache.solr.common.cloud.Aliases;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Abstract test class to setup shared mocks for unit testing v2 API calls that go to the Overseer
+ * or the DistributedCollectionConfigSetCommandRunner.
+ */
+public abstract class MockV2APITest extends SolrTestCaseJ4 {
+
+  protected CoreContainer mockCoreContainer;
+  protected ZkController mockZkController;
+  protected ClusterState mockClusterState;
+  protected DistributedCollectionConfigSetCommandRunner mockCommandRunner;
+  protected SolrZkClient mockSolrZkClient;
+  protected ZkStateReader mockZkStateReader;
+  protected SolrQueryRequest mockQueryRequest;
+  protected SolrQueryResponse queryResponse;
+  protected ArgumentCaptor<ZkNodeProps> messageCapturer;
+  protected ArgumentCaptor<AdminCmdContext> contextCapturer;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    mockCoreContainer = mock(CoreContainer.class);
+    mockZkController = mock(ZkController.class);
+    mockClusterState = mock(ClusterState.class);
+    mockSolrZkClient = mock(SolrZkClient.class);
+    mockZkStateReader = mock(ZkStateReader.class);
+    mockCommandRunner = mock(DistributedCollectionConfigSetCommandRunner.class);
+    when(mockCoreContainer.getZkController()).thenReturn(mockZkController);
+    when(mockCoreContainer.getAliases()).thenReturn(Aliases.EMPTY);
+    when(mockZkController.getDistributedCommandRunner()).thenReturn(Optional.of(mockCommandRunner));
+    when(mockZkController.getClusterState()).thenReturn(mockClusterState);
+    when(mockZkController.getZkStateReader()).thenReturn(mockZkStateReader);
+    when(mockZkController.getZkClient()).thenReturn(mockSolrZkClient);
+    when(mockCommandRunner.runCollectionCommand(any(), any(), anyLong()))
+        .thenReturn(new OverseerSolrResponse(new NamedList<>()));
+    mockQueryRequest = mock(SolrQueryRequest.class);
+    when(mockQueryRequest.getSpan()).thenReturn(Span.getInvalid());
+    queryResponse = new SolrQueryResponse();
+    messageCapturer = ArgumentCaptor.forClass(ZkNodeProps.class);
+    contextCapturer = ArgumentCaptor.forClass(AdminCmdContext.class);
+  }
+
+  protected void validateRunCommand(
+      CollectionParams.CollectionAction action,
+      String asyncId,
+      Consumer<Map<String, Object>> messageValidator) {
+    AdminCmdContext context = validateRunCommandInternal(action, messageValidator);
+    assertEquals("Wrong asyncId for command", asyncId, context.getAsyncId());
+  }
+
+  protected void validateRunCommand(
+      CollectionParams.CollectionAction action, Consumer<Map<String, Object>> messageValidator) {
+    AdminCmdContext context = validateRunCommandInternal(action, messageValidator);
+    assertNull("Command should have no asyncId", context.getAsyncId());
+  }
+
+  private AdminCmdContext validateRunCommandInternal(
+      CollectionParams.CollectionAction action, Consumer<Map<String, Object>> messageValidator) {
+    verify(mockCommandRunner)
+        .runCollectionCommand(contextCapturer.capture(), messageCapturer.capture(), anyLong());
+
+    var message = messageCapturer.getValue();
+    messageValidator.accept(message.getProperties());
+
+    AdminCmdContext context = contextCapturer.getValue();
+    assertEquals(action, context.getAction());
+    return context;
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCollectionAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCollectionAPITest.java
index 5900fece7a4..ee371a25cce 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCollectionAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCollectionAPITest.java
@@ -17,42 +17,53 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CoreAdminParams.NAME;
+import static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.ReloadCollectionRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link ReloadCollectionAPI} */
-public class ReloadCollectionAPITest extends SolrTestCaseJ4 {
+public class ReloadCollectionAPITest extends MockV2APITest {
+
+  private ReloadCollectionAPI api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new ReloadCollectionAPI(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
     final SolrException thrown =
         expectThrows(
             SolrException.class,
-            () -> {
-              final var api = new ReloadCollectionAPI(null, null, null);
-              api.reloadCollection(null, new ReloadCollectionRequestBody());
-            });
+            () -> api.reloadCollection(null, new ReloadCollectionRequestBody()));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
   }
 
-  // TODO message creation
   @Test
-  public void testCreateRemoteMessageAllProperties() {
+  public void testCreateRemoteMessageAllProperties() throws Exception {
     final var requestBody = new ReloadCollectionRequestBody();
     requestBody.async = "someAsyncId";
-    final var remoteMessage =
-        ReloadCollectionAPI.createRemoteMessage("someCollName", requestBody).getProperties();
 
-    assertEquals(3, remoteMessage.size());
-    assertEquals("reload", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollName", remoteMessage.get(NAME));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+    api.reloadCollection("someCollName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.RELOAD,
+        requestBody.async,
+        message -> {
+          assertEquals(1, message.size());
+          assertEquals("someCollName", message.get(NAME));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCoreAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCoreAPITest.java
index fc73c6781f9..1589f97310a 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCoreAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ReloadCoreAPITest.java
@@ -31,7 +31,7 @@ import org.junit.Test;
 
 public class ReloadCoreAPITest extends SolrTestCaseJ4 {
 
-  private ReloadCore reloadCoreAPI;
+  private ReloadCore api;
   private static final String NON_EXISTENT_CORE = "non_existent_core";
 
   @BeforeClass
@@ -49,13 +49,12 @@ public class ReloadCoreAPITest extends SolrTestCaseJ4 {
     CoreContainer coreContainer = h.getCoreContainer();
     CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker =
         new CoreAdminHandler.CoreAdminAsyncTracker();
-    reloadCoreAPI =
-        new ReloadCore(solrQueryRequest, solrQueryResponse, coreContainer, coreAdminAsyncTracker);
+    api = new ReloadCore(solrQueryRequest, solrQueryResponse, coreContainer, coreAdminAsyncTracker);
   }
 
   @Test
   public void testValidReloadCoreAPIResponse() throws Exception {
-    SolrJerseyResponse response = reloadCoreAPI.reloadCore(coreName, new ReloadCoreRequestBody());
+    SolrJerseyResponse response = api.reloadCore(coreName, new ReloadCoreRequestBody());
     assertEquals(0, response.responseHeader.status);
   }
 
@@ -64,9 +63,7 @@ public class ReloadCoreAPITest extends SolrTestCaseJ4 {
     final SolrException solrException =
         expectThrows(
             SolrException.class,
-            () -> {
-              reloadCoreAPI.reloadCore(NON_EXISTENT_CORE, new ReloadCoreRequestBody());
-            });
+            () -> api.reloadCore(NON_EXISTENT_CORE, new ReloadCoreRequestBody()));
     assertEquals(400, solrException.code());
     assertTrue(solrException.getMessage().contains("No such core: " + NON_EXISTENT_CORE));
   }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java
index a325da55e7d..7386c4e2967 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java
@@ -16,112 +16,72 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import java.util.Map;
-import java.util.Optional;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
-import org.apache.solr.cloud.OverseerSolrResponse;
-import org.apache.solr.cloud.ZkController;
-import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
-import org.apache.solr.common.cloud.ZkNodeProps;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.core.CoreContainer;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.common.params.CollectionParams;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /** Unit tests for {@link ReplaceNode} */
-public class ReplaceNodeAPITest extends SolrTestCaseJ4 {
+public class ReplaceNodeAPITest extends MockV2APITest {
 
-  private CoreContainer mockCoreContainer;
-  private ZkController mockZkController;
-  private SolrQueryRequest mockQueryRequest;
-  private SolrQueryResponse queryResponse;
-  private ReplaceNode replaceNodeApi;
-  private DistributedCollectionConfigSetCommandRunner mockCommandRunner;
-  private ArgumentCaptor<ZkNodeProps> messageCapturer;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
-  }
+  private ReplaceNode api;
 
   @Override
   @Before
   public void setUp() throws Exception {
     super.setUp();
-
-    mockCoreContainer = mock(CoreContainer.class);
-    mockZkController = mock(ZkController.class);
-    mockCommandRunner = mock(DistributedCollectionConfigSetCommandRunner.class);
-    when(mockCoreContainer.getZkController()).thenReturn(mockZkController);
-    when(mockZkController.getDistributedCommandRunner()).thenReturn(Optional.of(mockCommandRunner));
-    when(mockCommandRunner.runCollectionCommand(any(), any(), anyLong()))
-        .thenReturn(new OverseerSolrResponse(new NamedList<>()));
-    mockQueryRequest = mock(SolrQueryRequest.class);
-    queryResponse = new SolrQueryResponse();
-    replaceNodeApi = new ReplaceNode(mockCoreContainer, mockQueryRequest, queryResponse);
-    messageCapturer = ArgumentCaptor.forClass(ZkNodeProps.class);
-
     when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new ReplaceNode(mockCoreContainer, mockQueryRequest, queryResponse);
   }
 
   @Test
   public void testCreatesValidOverseerMessage() throws Exception {
     final var requestBody = new ReplaceNodeRequestBody("demoTargetNode", false, "async");
-    replaceNodeApi.replaceNode("demoSourceNode", requestBody);
-    verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
 
-    final ZkNodeProps createdMessage = messageCapturer.getValue();
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(5, createdMessageProps.size());
-    assertEquals("demoSourceNode", createdMessageProps.get("sourceNode"));
-    assertEquals("demoTargetNode", createdMessageProps.get("targetNode"));
-    assertEquals(false, createdMessageProps.get("waitForFinalState"));
-    assertEquals("async", createdMessageProps.get("async"));
-    assertEquals("replacenode", createdMessageProps.get("operation"));
+    api.replaceNode("demoSourceNode", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.REPLACENODE,
+        requestBody.async,
+        message -> {
+          assertEquals(3, message.size());
+          assertEquals("demoSourceNode", message.get("sourceNode"));
+          assertEquals("demoTargetNode", message.get("targetNode"));
+          assertEquals(false, message.get("waitForFinalState"));
+        });
   }
 
   @Test
   public void testRequestBodyCanBeOmittedAltogether() throws Exception {
-    replaceNodeApi.replaceNode("demoSourceNode", null);
-    verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
+    api.replaceNode("demoSourceNode", null);
 
-    final ZkNodeProps createdMessage = messageCapturer.getValue();
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
-    assertEquals(2, createdMessageProps.size());
-    assertEquals("demoSourceNode", createdMessageProps.get("sourceNode"));
-    assertEquals("replacenode", createdMessageProps.get("operation"));
+    validateRunCommand(
+        CollectionParams.CollectionAction.REPLACENODE,
+        message -> {
+          assertEquals(1, message.size());
+          assertEquals("demoSourceNode", message.get("sourceNode"));
+        });
   }
 
   @Test
   public void testOptionalValuesNotAddedToRemoteMessageIfNotProvided() throws Exception {
     final var requestBody = new ReplaceNodeRequestBody("demoTargetNode", null, null);
-    replaceNodeApi.replaceNode("demoSourceNode", requestBody);
-    verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
 
-    final ZkNodeProps createdMessage = messageCapturer.getValue();
-    final Map<String, Object> createdMessageProps = createdMessage.getProperties();
+    api.replaceNode("demoSourceNode", requestBody);
 
-    assertEquals(3, createdMessageProps.size());
-    assertEquals("demoSourceNode", createdMessageProps.get("sourceNode"));
-    assertEquals("demoTargetNode", createdMessageProps.get("targetNode"));
-    assertEquals("replacenode", createdMessageProps.get("operation"));
-    assertFalse(
-        "Expected message to not contain value for waitForFinalState: "
-            + createdMessageProps.get("waitForFinalState"),
-        createdMessageProps.containsKey("waitForFinalState"));
-    assertFalse(
-        "Expected message to not contain value for async: " + createdMessageProps.get("async"),
-        createdMessageProps.containsKey("async"));
+    validateRunCommand(
+        CollectionParams.CollectionAction.REPLACENODE,
+        message -> {
+          assertEquals(2, message.size());
+          assertEquals("demoSourceNode", message.get("sourceNode"));
+          assertEquals("demoTargetNode", message.get("targetNode"));
+          assertFalse(
+              "Expected message to not contain value for waitForFinalState: "
+                  + message.get("waitForFinalState"),
+              message.containsKey("waitForFinalState"));
+        });
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/RestoreCollectionAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/RestoreCollectionAPITest.java
index ce9833771f9..de786a182de 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/RestoreCollectionAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/RestoreCollectionAPITest.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
 import static org.apache.solr.common.params.CollectionAdminParams.COLL_CONF;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
@@ -25,40 +24,49 @@ import static org.apache.solr.common.params.CollectionAdminParams.NRT_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.PULL_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.REPLICATION_FACTOR;
 import static org.apache.solr.common.params.CollectionAdminParams.TLOG_REPLICAS;
-import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_ID;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
 import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
 import static org.apache.solr.common.params.CoreAdminParams.NAME;
 import static org.hamcrest.Matchers.containsString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import java.net.URI;
 import java.util.List;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CreateCollectionRequestBody;
 import org.apache.solr.client.api.model.RestoreCollectionRequestBody;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.NodeConfig;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.junit.BeforeClass;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link RestoreCollection} */
-public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
-
-  private static RestoreCollection restoreCollectionAPI;
-
-  @BeforeClass
-  public static void setUpApi() {
-    restoreCollectionAPI =
-        new RestoreCollection(
-            new CoreContainer(
-                new NodeConfig.NodeConfigBuilder("testnode", createTempDir()).build()),
-            new LocalSolrQueryRequest(null, new NamedList<>()),
-            null);
+public class RestoreCollectionAPITest extends MockV2APITest {
+
+  private static RestoreCollection api;
+  private BackupRepository mockBackupRepository;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    mockBackupRepository = mock(BackupRepository.class);
+    when(mockCoreContainer.newBackupRepository(eq("someRepository")))
+        .thenReturn(mockBackupRepository);
+    when(mockCoreContainer.newBackupRepository(null)).thenReturn(mockBackupRepository);
+    when(mockBackupRepository.getBackupLocation(eq("someLocation"))).thenReturn("someLocation");
+    URI uri = new URI("someLocation");
+    when(mockBackupRepository.createDirectoryURI(eq("someLocation"))).thenReturn(uri);
+    when(mockBackupRepository.exists(eq(uri))).thenReturn(true);
+
+    api = new RestoreCollection(mockCoreContainer, mockQueryRequest, queryResponse);
   }
 
   @Test
@@ -66,11 +74,7 @@ public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
     final var requestBody = new RestoreCollectionRequestBody();
     requestBody.collection = "someCollection";
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              restoreCollectionAPI.restoreCollection(null, requestBody);
-            });
+        expectThrows(SolrException.class, () -> api.restoreCollection(null, requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Required parameter 'backupName' missing", thrown.getMessage());
@@ -79,11 +83,7 @@ public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
   @Test
   public void testReportsErrorIfRequestBodyMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              restoreCollectionAPI.restoreCollection("someBackupName", null);
-            });
+        expectThrows(SolrException.class, () -> api.restoreCollection("someBackupName", null));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required request body", thrown.getMessage());
@@ -95,10 +95,7 @@ public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
     final var requestBody = new RestoreCollectionRequestBody();
     final SolrException thrown =
         expectThrows(
-            SolrException.class,
-            () -> {
-              restoreCollectionAPI.restoreCollection("someBackupName", requestBody);
-            });
+            SolrException.class, () -> api.restoreCollection("someBackupName", requestBody));
 
     assertEquals(400, thrown.code());
     assertEquals("Required parameter 'collection' missing", thrown.getMessage());
@@ -110,10 +107,7 @@ public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
     requestBody.collection = "invalid$collection@name";
     final SolrException thrown =
         expectThrows(
-            SolrException.class,
-            () -> {
-              restoreCollectionAPI.restoreCollection("someBackupName", requestBody);
-            });
+            SolrException.class, () -> api.restoreCollection("someBackupName", requestBody));
 
     assertEquals(400, thrown.code());
     assertThat(
@@ -121,34 +115,37 @@ public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
   }
 
   @Test
-  public void testCreatesValidRemoteMessageForExistingCollectionRestore() {
+  public void testCreatesValidRemoteMessageForExistingCollectionRestore() throws Exception {
     final var requestBody = new RestoreCollectionRequestBody();
     requestBody.collection = "someCollectionName";
-    requestBody.location = "/some/location/path";
+    requestBody.location = "someLocation";
     requestBody.backupId = 123;
-    requestBody.repository = "someRepositoryName";
+    requestBody.repository = "someRepository";
     requestBody.async = "someAsyncId";
 
-    final var remoteMessage =
-        restoreCollectionAPI.createRemoteMessage("someBackupName", requestBody).getProperties();
-
-    assertEquals(7, remoteMessage.size());
-    assertEquals("restore", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollectionName", remoteMessage.get(COLLECTION));
-    assertEquals("/some/location/path", remoteMessage.get(BACKUP_LOCATION));
-    assertEquals(Integer.valueOf(123), remoteMessage.get(BACKUP_ID));
-    assertEquals("someRepositoryName", remoteMessage.get(BACKUP_REPOSITORY));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
-    assertEquals("someBackupName", remoteMessage.get(NAME));
+    when(mockClusterState.hasCollection(eq("someCollectionName"))).thenReturn(true);
+    api.restoreCollection("someBackupName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.RESTORE,
+        requestBody.async,
+        message -> {
+          assertEquals(5, message.size());
+          assertEquals("someCollectionName", message.get(COLLECTION));
+          assertEquals("someLocation", message.get(BACKUP_LOCATION));
+          assertEquals(Integer.valueOf(123), message.get(BACKUP_ID));
+          assertEquals("someRepository", message.get(BACKUP_REPOSITORY));
+          assertEquals("someBackupName", message.get(NAME));
+        });
   }
 
   @Test
-  public void testCreatesValidRemoteMessageForNewCollectionRestore() {
+  public void testCreatesValidRemoteMessageForNewCollectionRestore() throws Exception {
     final var requestBody = new RestoreCollectionRequestBody();
     requestBody.collection = "someCollectionName";
-    requestBody.location = "/some/location/path";
+    requestBody.location = "someLocation";
     requestBody.backupId = 123;
-    requestBody.repository = "someRepositoryName";
+    requestBody.repository = "someRepository";
     requestBody.async = "someAsyncId";
     final var createParams = new CreateCollectionRequestBody();
     requestBody.createCollectionParams = createParams;
@@ -159,24 +156,27 @@ public class RestoreCollectionAPITest extends SolrTestCaseJ4 {
     createParams.nodeSet = List.of("node1", "node2");
     createParams.properties = Map.of("foo", "bar");
 
-    final var remoteMessage =
-        restoreCollectionAPI.createRemoteMessage("someBackupName", requestBody).getProperties();
-
-    assertEquals(14, remoteMessage.size());
-    assertEquals("restore", remoteMessage.get(QUEUE_OPERATION));
-    assertEquals("someCollectionName", remoteMessage.get(COLLECTION));
-    assertEquals("/some/location/path", remoteMessage.get(BACKUP_LOCATION));
-    assertEquals(Integer.valueOf(123), remoteMessage.get(BACKUP_ID));
-    assertEquals("someRepositoryName", remoteMessage.get(BACKUP_REPOSITORY));
-    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
-    assertEquals("someBackupName", remoteMessage.get(NAME));
-    assertEquals("someConfig", remoteMessage.get(COLL_CONF));
-    assertEquals(Integer.valueOf(123), remoteMessage.get(NRT_REPLICAS));
-    assertEquals(Integer.valueOf(123), remoteMessage.get(REPLICATION_FACTOR));
-    assertEquals(Integer.valueOf(456), remoteMessage.get(TLOG_REPLICAS));
-    assertEquals(Integer.valueOf(789), remoteMessage.get(PULL_REPLICAS));
-    assertEquals("node1,node2", remoteMessage.get(CREATE_NODE_SET_PARAM));
-    assertEquals("bar", remoteMessage.get("property.foo"));
+    when(mockClusterState.hasCollection(eq("someCollectionName"))).thenReturn(true);
+    api.restoreCollection("someBackupName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.RESTORE,
+        requestBody.async,
+        message -> {
+          assertEquals(12, message.size());
+          assertEquals("someCollectionName", message.get(COLLECTION));
+          assertEquals("someLocation", message.get(BACKUP_LOCATION));
+          assertEquals(Integer.valueOf(123), message.get(BACKUP_ID));
+          assertEquals("someRepository", message.get(BACKUP_REPOSITORY));
+          assertEquals("someBackupName", message.get(NAME));
+          assertEquals("someConfig", message.get(COLL_CONF));
+          assertEquals(Integer.valueOf(123), message.get(NRT_REPLICAS));
+          assertEquals(Integer.valueOf(123), message.get(REPLICATION_FACTOR));
+          assertEquals(Integer.valueOf(456), message.get(TLOG_REPLICAS));
+          assertEquals(Integer.valueOf(789), message.get(PULL_REPLICAS));
+          assertEquals("node1,node2", message.get(CREATE_NODE_SET_PARAM));
+          assertEquals("bar", message.get("property.foo"));
+        });
   }
 
   @Test
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java
index 23d3f8982a2..cb80ade0d37 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java
@@ -17,21 +17,30 @@
 
 package org.apache.solr.handler.admin.api;
 
-import org.apache.solr.SolrTestCaseJ4;
+import static org.mockito.Mockito.when;
+
 import org.apache.solr.common.SolrException;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link SyncShard} */
-public class SyncShardAPITest extends SolrTestCaseJ4 {
+public class SyncShardAPITest extends MockV2APITest {
+
+  private SyncShard api;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    api = new SyncShard(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new SyncShard(null, null, null);
-              api.syncShard(null, "someShard");
-            });
+        expectThrows(SolrException.class, () -> api.syncShard(null, "someShard"));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: collection", thrown.getMessage());
@@ -40,12 +49,7 @@ public class SyncShardAPITest extends SolrTestCaseJ4 {
   @Test
   public void testReportsErrorIfShardNameMissing() {
     final SolrException thrown =
-        expectThrows(
-            SolrException.class,
-            () -> {
-              final var api = new SyncShard(null, null, null);
-              api.syncShard("someCollection", null);
-            });
+        expectThrows(SolrException.class, () -> api.syncShard("someCollection", null));
 
     assertEquals(400, thrown.code());
     assertEquals("Missing required parameter: shard", thrown.getMessage());
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/UnloadCoreAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/UnloadCoreAPITest.java
index c823f33dea1..c2c8a3b126e 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/UnloadCoreAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/UnloadCoreAPITest.java
@@ -30,7 +30,7 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class UnloadCoreAPITest extends SolrTestCaseJ4 {
-  private UnloadCore unloadCoreAPI;
+  private UnloadCore api;
   private static final String NON_EXISTENT_CORE = "non_existent_core";
 
   @BeforeClass
@@ -48,13 +48,12 @@ public class UnloadCoreAPITest extends SolrTestCaseJ4 {
     CoreContainer coreContainer = h.getCoreContainer();
     CoreAdminHandler.CoreAdminAsyncTracker coreAdminAsyncTracker =
         new CoreAdminHandler.CoreAdminAsyncTracker();
-    unloadCoreAPI =
-        new UnloadCore(coreContainer, coreAdminAsyncTracker, solrQueryRequest, solrQueryResponse);
+    api = new UnloadCore(coreContainer, coreAdminAsyncTracker, solrQueryRequest, solrQueryResponse);
   }
 
   @Test
   public void testValidUnloadCoreAPIResponse() throws Exception {
-    SolrJerseyResponse response = unloadCoreAPI.unloadCore(coreName, getUnloadCoreRequestBodyObj());
+    SolrJerseyResponse response = api.unloadCore(coreName, getUnloadCoreRequestBodyObj());
     assertEquals(0, response.responseHeader.status);
   }
 
@@ -63,9 +62,7 @@ public class UnloadCoreAPITest extends SolrTestCaseJ4 {
     final SolrException solrException =
         expectThrows(
             SolrException.class,
-            () -> {
-              unloadCoreAPI.unloadCore(NON_EXISTENT_CORE, getUnloadCoreRequestBodyObj());
-            });
+            () -> api.unloadCore(NON_EXISTENT_CORE, getUnloadCoreRequestBodyObj()));
     assertEquals(400, solrException.code());
     assertTrue(solrException.getMessage().contains("Cannot unload non-existent core"));
   }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
index 514423ba5df..6f36607bfcc 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
@@ -16,18 +16,40 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionAdminParams.COPY_FILES_STRATEGY;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
-import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CreateCollectionBackupRequestBody;
+import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Unit tests for {@link CreateCollectionBackup} */
-public class V2CollectionBackupApiTest extends SolrTestCaseJ4 {
+public class V2CollectionBackupApiTest extends MockV2APITest {
+
+  private CreateCollectionBackup api;
+  private BackupRepository mockBackupRepository;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+    mockBackupRepository = mock(BackupRepository.class);
+    when(mockCoreContainer.newBackupRepository("someRepoName")).thenReturn(mockBackupRepository);
+    when(mockCoreContainer.newBackupRepository(null)).thenReturn(mockBackupRepository);
+
+    api = new CreateCollectionBackup(mockCoreContainer, mockQueryRequest, queryResponse);
+  }
+
   @Test
-  public void testCreateRemoteMessageWithAllProperties() {
+  public void testCreateRemoteMessageWithAllProperties() throws Exception {
+
     final var requestBody = new CreateCollectionBackupRequestBody();
     requestBody.location = "/some/location";
     requestBody.repository = "someRepoName";
@@ -38,44 +60,58 @@ public class V2CollectionBackupApiTest extends SolrTestCaseJ4 {
     requestBody.maxNumBackupPoints = 123;
     requestBody.async = "someId";
 
-    var message =
-        CreateCollectionBackup.createRemoteMessage(
-            "someCollectionName", "someBackupName", requestBody);
-    var messageProps = message.getProperties();
-
-    assertEquals(11, messageProps.size());
-    assertEquals("someCollectionName", messageProps.get("collection"));
-    assertEquals("/some/location", messageProps.get("location"));
-    assertEquals("someRepoName", messageProps.get("repository"));
-    assertEquals(true, messageProps.get("followAliases"));
-    assertEquals("copy-files", messageProps.get("indexBackup"));
-    assertEquals("someSnapshotName", messageProps.get("commitName"));
-    assertEquals(true, messageProps.get("incremental"));
-    assertEquals(123, messageProps.get("maxNumBackupPoints"));
-    assertEquals("someId", messageProps.get("async"));
-    assertEquals("backup", messageProps.get(QUEUE_OPERATION));
-    assertEquals("someBackupName", messageProps.get("name"));
+    when(mockClusterState.hasCollection("someCollectionName")).thenReturn(true);
+    when(mockBackupRepository.getBackupLocation(requestBody.location))
+        .thenReturn(requestBody.location);
+    when(mockBackupRepository.exists(any())).thenReturn(true);
+
+    api.createCollectionBackup("someCollectionName", "someBackupName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.BACKUP,
+        requestBody.async,
+        message -> {
+          assertEquals(message.toString(), 9, message.size());
+          assertEquals("someCollectionName", message.get("collection"));
+          assertEquals("/some/location", message.get("location"));
+          assertEquals("someRepoName", message.get("repository"));
+          assertEquals(true, message.get("followAliases"));
+          assertEquals("copy-files", message.get("indexBackup"));
+          assertEquals("someSnapshotName", message.get("commitName"));
+          assertEquals(true, message.get("incremental"));
+          assertEquals(123, message.get("maxNumBackupPoints"));
+          assertEquals("someBackupName", message.get("name"));
+        });
   }
 
   @Test
-  public void testCreateRemoteMessageOmitsNullValues() {
+  public void testCreateRemoteMessageOmitsNullValues() throws Exception {
     final var requestBody = new CreateCollectionBackupRequestBody();
     requestBody.location = "/some/location";
 
-    var message =
-        CreateCollectionBackup.createRemoteMessage(
-            "someCollectionName", "someBackupName", requestBody);
-    var messageProps = message.getProperties();
+    when(mockClusterState.hasCollection("someCollectionName")).thenReturn(true);
+    when(mockBackupRepository.getBackupLocation(requestBody.location))
+        .thenReturn(requestBody.location);
+    when(mockBackupRepository.exists(any())).thenReturn(true);
 
-    assertEquals(4, messageProps.size());
-    assertEquals("someCollectionName", messageProps.get("collection"));
-    assertEquals("/some/location", messageProps.get("location"));
-    assertEquals("backup", messageProps.get(QUEUE_OPERATION));
-    assertEquals("someBackupName", messageProps.get("name"));
+    api.createCollectionBackup("someCollectionName", "someBackupName", requestBody);
+
+    validateRunCommand(
+        CollectionParams.CollectionAction.BACKUP,
+        message -> {
+          assertEquals(5, message.size());
+          assertEquals("someCollectionName", message.get("collection"));
+          assertEquals("/some/location", message.get("location"));
+          assertEquals("someBackupName", message.get("name"));
+          assertEquals(true, message.get("incremental"));
+          assertEquals("copy-files", message.get("indexBackup"));
+        });
   }
 
   @Test
   public void testCanCreateV2RequestBodyFromV1Params() {
+    when(mockClusterState.hasCollection("demoSourceNode")).thenReturn(true);
+
     final var params = new ModifiableSolrParams();
     params.set("collection", "someCollectionName");
     params.set("location", "/some/location");
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/response/RequestStatusState.java b/solr/solrj/src/java/org/apache/solr/client/solrj/response/RequestStatusState.java
index a6166fb97c2..e03efed81d6 100755
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/response/RequestStatusState.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/response/RequestStatusState.java
@@ -26,24 +26,26 @@ import java.util.Locale;
 public enum RequestStatusState {
 
   /** The request was completed. */
-  COMPLETED("completed"),
+  COMPLETED("completed", true),
 
   /** The request has failed. */
-  FAILED("failed"),
+  FAILED("failed", true),
 
   /** The request is in progress. */
-  RUNNING("running"),
+  RUNNING("running", false),
 
   /** The request was submitted, but has not yet started. */
-  SUBMITTED("submitted"),
+  SUBMITTED("submitted", false),
 
   /** The request was not found. */
-  NOT_FOUND("notfound");
+  NOT_FOUND("notfound", true);
 
   private final String key;
+  private final boolean isFinal;
 
-  private RequestStatusState(String key) {
+  RequestStatusState(String key, boolean isFinal) {
     this.key = key;
+    this.isFinal = isFinal;
   }
 
   /**
@@ -54,6 +56,11 @@ public enum RequestStatusState {
     return key;
   }
 
+  /** Returns whether this state should be considered the final state for the request status. */
+  public boolean isFinal() {
+    return isFinal;
+  }
+
   /**
    * Resolves a key that was returned from {@link #getKey()} to a {@link RequestStatusState}. For
    * backwards compatibility, it resolves the key "notfound" to {@link #NOT_FOUND}.
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/RetryUtil.java b/solr/solrj/src/java/org/apache/solr/common/util/RetryUtil.java
index 9e890a79ffa..7c6c7c5dc55 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/RetryUtil.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/RetryUtil.java
@@ -20,6 +20,7 @@ import java.lang.invoke.MethodHandles;
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.slf4j.Logger;
@@ -38,6 +39,15 @@ public class RetryUtil {
     void execute() throws Exception;
   }
 
+  /**
+   * Interface for typed commands that can be retried and may throw exceptions.
+   *
+   * <p>Implementations should define the operation to be executed in the {@link #execute()} method.
+   */
+  public interface TypedRetryCmd<T> {
+    T execute() throws Exception;
+  }
+
   /**
    * Interface for commands that return a boolean result indicating success or failure.
    *
@@ -144,6 +154,41 @@ public class RetryUtil {
     throw new SolrException(ErrorCode.SERVER_ERROR, errorMessage);
   }
 
+  /**
+   * Retries a command until it returns a value that matches the given test or the maximum number of
+   * retries is reached.
+   *
+   * <p>The command is executed up to {@code retries} times. After each failed attempt (when the
+   * command returns a value that returns {@code false} when given to the {@code test}), the method
+   * pauses for the specified duration before retrying. If all retries are exhausted without
+   * success, a {@link SolrException} is thrown with the provided error message.
+   *
+   * @param errorMessage the error message to use if all retries are exhausted
+   * @param retries maximum number of retry attempts
+   * @param pauseTime duration to pause between retries
+   * @param pauseUnit time unit for the pause duration
+   * @param cmd the command to execute
+   * @param test the predicate to test against the value produced by {@code cmd}
+   * @throws Exception if the thread is interrupted while sleeping between retries or the cmd throws
+   *     an exception
+   * @throws SolrException if all retries are exhausted without success
+   */
+  public static <T> T retryUntil(
+      String errorMessage,
+      int retries,
+      long pauseTime,
+      TimeUnit pauseUnit,
+      TypedRetryCmd<T> cmd,
+      Predicate<T> test)
+      throws Exception {
+    while (retries-- > 0) {
+      T value = cmd.execute();
+      if (test.test(value)) return value;
+      pauseUnit.sleep(pauseTime);
+    }
+    throw new SolrException(ErrorCode.SERVER_ERROR, errorMessage);
+  }
+
   /**
    * Retries a command until it returns {@code true} or the timeout is reached.
    *
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
index f1c62f17ee8..20cf4abd452 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
@@ -44,6 +44,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.security.SecureRandom;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -78,12 +79,14 @@ import org.apache.lucene.tests.util.LuceneTestCase;
 import org.apache.lucene.tests.util.LuceneTestCase.SuppressFileSystems;
 import org.apache.lucene.tests.util.TestUtil;
 import org.apache.lucene.util.Constants;
+import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
 import org.apache.solr.client.solrj.apache.HttpClientUtil;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.impl.ClusterStateProvider;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.response.SolrResponseBase;
 import org.apache.solr.client.solrj.util.ClientUtils;
 import org.apache.solr.cloud.IpTables;
@@ -102,6 +105,7 @@ import org.apache.solr.common.params.UpdateParams;
 import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.IOUtils;
+import org.apache.solr.common.util.RetryUtil;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.XML;
@@ -2502,6 +2506,18 @@ public abstract class SolrTestCaseJ4 extends SolrTestCase {
     return (0 == TestUtil.nextInt(random(), 0, 9)) ? unlikely : likely;
   }
 
+  public static CollectionAdminRequest.RequestStatusResponse waitForAsyncClusterRequest(
+      SolrClient solrClient, String asyncId, Duration timeout) throws Exception {
+    final var requestStatus = CollectionAdminRequest.requestStatus(asyncId);
+    return RetryUtil.retryUntil(
+        "Async request " + asyncId + " did not complete within duration: " + timeout,
+        (int) (timeout.toMillis() / 50),
+        50,
+        TimeUnit.MILLISECONDS,
+        () -> requestStatus.process(solrClient),
+        rsp -> rsp.getRequestStatus().isFinal());
+  }
+
   /**
    * A variant of {@link org.apache.solr.client.solrj.impl.CloudSolrClient.Builder} that will
    * randomize some internal settings.
diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudTestCase.java b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudTestCase.java
index 7f589e48b23..b8512f56317 100644
--- a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudTestCase.java
+++ b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudTestCase.java
@@ -26,6 +26,7 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 import java.lang.invoke.MethodHandles;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -47,6 +48,7 @@ import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.CoreAdminRequest;
 import org.apache.solr.common.cloud.CollectionStatePredicate;
 import org.apache.solr.common.cloud.DocCollection;
@@ -415,6 +417,11 @@ public class SolrCloudTestCase extends SolrTestCaseJ4 {
     }
   }
 
+  protected CollectionAdminRequest.RequestStatusResponse waitForAsyncClusterRequest(
+      String asyncId, Duration timeout) throws Exception {
+    return waitForAsyncClusterRequest(cluster.getSolrClient(), asyncId, timeout);
+  }
+
   @SuppressWarnings({"rawtypes"})
   protected NamedList waitForResponse(
       Predicate<NamedList> predicate,