This is an automated email from the ASF dual-hosted git repository.
epugh 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 1fdb455a524 SOLR-16398: Migrate split shard v2 API to JAX-RS (#5265)
1fdb455a524 is described below
commit 1fdb455a524a7b8d334d5baea04e395ed4d2d7f8
Author: Nick <[email protected]>
AuthorDate: Tue Nov 3 09:44:13 2026 -0700
SOLR-16398: Migrate split shard v2 API to JAX-RS (#5265)
---
changelog/unreleased/SOLR-16398.yml | 7 +
.../solr/client/api/endpoint/SplitShardApi.java | 39 +++
.../client/api/model/SplitShardRequestBody.java | 53 +++
.../solr/client/api/model/SplitShardResponse.java | 27 ++
.../solr/handler/admin/CollectionsHandler.java | 64 +---
.../solr/handler/admin/api/AdminAPIBase.java | 44 ++-
.../apache/solr/handler/admin/api/SplitShard.java | 213 +++++++++++
.../solr/handler/admin/api/SplitShardAPI.java | 74 ----
.../solr/handler/admin/TestApiFramework.java | 10 +-
.../solr/handler/admin/TestCollectionAPIs.java | 14 -
.../solr/handler/admin/api/SplitShardAPITest.java | 139 ++++++++
.../admin/api/SplitShardMessageMappingTest.java | 390 +++++++++++++++++++++
.../handler/admin/api/V2ShardsAPIMappingTest.java | 106 ------
13 files changed, 915 insertions(+), 265 deletions(-)
diff --git a/changelog/unreleased/SOLR-16398.yml
b/changelog/unreleased/SOLR-16398.yml
new file mode 100644
index 00000000000..f49fa1992c2
--- /dev/null
+++ b/changelog/unreleased/SOLR-16398.yml
@@ -0,0 +1,7 @@
+title: Migrate the v2 split-shard API to JAX-RS
+type: added
+authors:
+ - name: Nick Shanin
+links:
+ - name: SOLR-16398
+ url: https://issues.apache.org/jira/browse/SOLR-16398
diff --git
a/solr/api/src/java/org/apache/solr/client/api/endpoint/SplitShardApi.java
b/solr/api/src/java/org/apache/solr/client/api/endpoint/SplitShardApi.java
new file mode 100644
index 00000000000..3dd0daec495
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SplitShardApi.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.SplitShardRequestBody;
+import org.apache.solr.client.api.model.SplitShardResponse;
+
+@Path("/collections/{collectionName}/shards/{shardName}/split")
+public interface SplitShardApi {
+ @POST
+ @Operation(
+ summary = "Split an existing shard into two or more new shards.",
+ tags = {"shards"})
+ SplitShardResponse splitShard(
+ @PathParam("collectionName") String collectionName,
+ @PathParam("shardName") String shardName,
+ @RequestBody(description = "Contains user provided parameters")
+ SplitShardRequestBody requestBody)
+ throws Exception;
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/SplitShardRequestBody.java
b/solr/api/src/java/org/apache/solr/client/api/model/SplitShardRequestBody.java
new file mode 100644
index 00000000000..600f54a4996
--- /dev/null
+++
b/solr/api/src/java/org/apache/solr/client/api/model/SplitShardRequestBody.java
@@ -0,0 +1,53 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+
+public class SplitShardRequestBody {
+ @JsonProperty public String ranges;
+
+ @JsonProperty public String splitKey;
+
+ @JsonProperty public Integer numSubShards;
+
+ @JsonProperty public String splitFuzz;
+
+ @JsonProperty public Boolean timing;
+
+ @JsonProperty public Boolean splitByPrefix;
+
+ @JsonProperty public Boolean followAliases;
+
+ @JsonProperty public String splitMethod;
+
+ @JsonProperty public String createNodeSet;
+
+ @JsonProperty public Map<String, Object> coreProperties;
+
+ @JsonProperty public String async;
+
+ /**
+ * @deprecated Solr is moving toward always waiting for final state, with no
option to opt out;
+ * once that happens, this parameter will have no effect and will likely
be removed. See
+ * SOLR-17712.
+ */
+ @JsonProperty
+ @Deprecated(since = "9.10")
+ public Boolean waitForFinalState;
+}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/SplitShardResponse.java
b/solr/api/src/java/org/apache/solr/client/api/model/SplitShardResponse.java
new file mode 100644
index 00000000000..58e96337e49
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/SplitShardResponse.java
@@ -0,0 +1,27 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+
+/** Response from splitting a shard, including optional per-phase timing
information. */
+public class SplitShardResponse extends SubResponseAccumulatingJerseyResponse {
+
+ @JsonProperty public Map<String, Object> timing;
+}
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 28d2e3af535..4fe1f3a9ba2 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
@@ -30,7 +30,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.CALLING_LOCK_ID_HEADER;
import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static
org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
import static
org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
import static
org.apache.solr.common.params.CollectionAdminParams.PROPERTY_NAME;
import static
org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
@@ -83,13 +82,8 @@ 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.IN_PLACE_MOVE;
-import static org.apache.solr.common.params.CommonAdminParams.NUM_SUB_SHARDS;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_BY_PREFIX;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_FUZZ;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
import static
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.common.params.CommonParams.TIMING;
import static org.apache.solr.common.params.CommonParams.VALUE_LONG;
import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
@@ -203,7 +197,7 @@ import
org.apache.solr.handler.admin.api.ReloadCollectionAPI;
import org.apache.solr.handler.admin.api.RenameCollection;
import org.apache.solr.handler.admin.api.ReplaceNode;
import org.apache.solr.handler.admin.api.RestoreCollection;
-import org.apache.solr.handler.admin.api.SplitShardAPI;
+import org.apache.solr.handler.admin.api.SplitShard;
import org.apache.solr.handler.admin.api.SyncShard;
import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.logging.MDCLoggingContext;
@@ -638,61 +632,15 @@ public class CollectionsHandler extends
RequestHandlerBase implements Permission
return null;
}),
/**
- * V2 equivalent: {@link SplitShardAPI} ({@code POST
/collections/{collection}/shards} with the
- * {@code split} command).
+ * V2 equivalent: {@link org.apache.solr.handler.admin.api.SplitShard}
({@code POST
+ * /collections/{collection}/shards/{shard}/split}).
*/
SPLITSHARD_OP(
SPLITSHARD,
DEFAULT_COLLECTION_OP_TIMEOUT * 5,
(req, rsp, h) -> {
- String name = req.getParams().required().get(COLLECTION_PROP);
- // TODO : add support for multiple shards
- String shard = req.getParams().get(SHARD_ID_PROP);
- String rangesStr = req.getParams().get(CoreAdminParams.RANGES);
- String splitKey = req.getParams().get("split.key");
- String numSubShards = req.getParams().get(NUM_SUB_SHARDS);
- String fuzz = req.getParams().get(SPLIT_FUZZ);
-
- if (splitKey == null && shard == null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST, "At least one of shard, or split.key
should be specified.");
- }
- if (splitKey != null && shard != null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST, "Only one of 'shard' or 'split.key'
should be specified");
- }
- if (splitKey != null && rangesStr != null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST, "Only one of 'ranges' or 'split.key'
should be specified");
- }
- if (numSubShards != null && (splitKey != null || rangesStr != null))
{
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "numSubShards can not be specified with split.key or ranges
parameters");
- }
- if (fuzz != null && (splitKey != null || rangesStr != null)) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "fuzz can not be specified with split.key or ranges
parameters");
- }
-
- Map<String, Object> map =
- copy(
- req.getParams(),
- null,
- COLLECTION_PROP,
- SHARD_ID_PROP,
- "split.key",
- CoreAdminParams.RANGES,
- WAIT_FOR_FINAL_STATE,
- TIMING,
- SPLIT_METHOD,
- NUM_SUB_SHARDS,
- SPLIT_FUZZ,
- SPLIT_BY_PREFIX,
- FOLLOW_ALIASES,
- CREATE_NODE_SET_PARAM);
- return copyPropertiesWithPrefix(req.getParams(), map,
PROPERTY_PREFIX);
+ SplitShard.invokeWithV1Params(h.coreContainer, req, rsp);
+ return null;
}),
DELETESHARD_OP(
DELETESHARD,
@@ -1245,6 +1193,7 @@ public class CollectionsHandler extends
RequestHandlerBase implements Permission
MigrateReplicas.class,
BalanceReplicas.class,
RestoreCollection.class,
+ SplitShard.class,
SyncShard.class,
CollectionProperty.class,
DeleteNode.class,
@@ -1260,7 +1209,6 @@ public class CollectionsHandler extends
RequestHandlerBase implements Permission
@Override
public Collection<Api> getApis() {
final List<Api> apis = new ArrayList<>();
- apis.addAll(AnnotatedApi.getApis(new SplitShardAPI(this)));
apis.addAll(AnnotatedApi.getApis(new MigrateDocsAPI(this)));
apis.addAll(AnnotatedApi.getApis(new ModifyCollectionAPI(this)));
apis.addAll(AnnotatedApi.getApis(new MoveReplicaAPI(this)));
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
index cc270e61994..8cb671cffd5 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
@@ -130,12 +130,19 @@ public abstract class AdminAPIBase extends JerseyResource
{
protected SolrResponse submitRemoteMessageAndHandleException(
SolrJerseyResponse response, AdminCmdContext adminCmdContext,
ZkNodeProps remoteMessage)
throws Exception {
+ return submitRemoteMessageAndHandleException(
+ response, adminCmdContext, remoteMessage,
DEFAULT_COLLECTION_OP_TIMEOUT);
+ }
+
+ protected SolrResponse submitRemoteMessageAndHandleException(
+ SolrJerseyResponse response,
+ AdminCmdContext adminCmdContext,
+ ZkNodeProps remoteMessage,
+ long timeoutMs)
+ throws Exception {
final SolrResponse remoteResponse =
CollectionsHandler.submitCollectionApiCommand(
- coreContainer.getZkController(),
- adminCmdContext,
- remoteMessage,
- DEFAULT_COLLECTION_OP_TIMEOUT);
+ coreContainer.getZkController(), adminCmdContext, remoteMessage,
timeoutMs);
if (remoteResponse.getException() != null) {
throw remoteResponse.getException();
}
@@ -157,9 +164,23 @@ public abstract class AdminAPIBase extends JerseyResource {
ZkNodeProps remoteMessage,
String asyncId)
throws Exception {
+ return submitRemoteMessageAndHandleAsync(
+ response, action, remoteMessage, asyncId,
DEFAULT_COLLECTION_OP_TIMEOUT);
+ }
+
+ protected SolrResponse submitRemoteMessageAndHandleAsync(
+ AsyncJerseyResponse response,
+ CollectionParams.CollectionAction action,
+ ZkNodeProps remoteMessage,
+ String asyncId,
+ long timeoutMs)
+ throws Exception {
var remoteResponse =
submitRemoteMessageAndHandleException(
- response, new AdminCmdContext(action, asyncId, callingLockId),
remoteMessage);
+ response,
+ new AdminCmdContext(action, asyncId, callingLockId),
+ remoteMessage,
+ timeoutMs);
if (asyncId != null) {
response.requestId = asyncId;
@@ -174,8 +195,19 @@ public abstract class AdminAPIBase extends JerseyResource {
ZkNodeProps remoteMessage,
String asyncId)
throws Exception {
+ return submitRemoteMessageAndHandleResponse(
+ response, action, remoteMessage, asyncId,
DEFAULT_COLLECTION_OP_TIMEOUT);
+ }
+
+ protected SolrResponse submitRemoteMessageAndHandleResponse(
+ SubResponseAccumulatingJerseyResponse response,
+ CollectionParams.CollectionAction action,
+ ZkNodeProps remoteMessage,
+ String asyncId,
+ long timeoutMs)
+ throws Exception {
var remoteResponse =
- submitRemoteMessageAndHandleAsync(response, action, remoteMessage,
asyncId);
+ submitRemoteMessageAndHandleAsync(response, action, remoteMessage,
asyncId, timeoutMs);
if (asyncId != null) {
response.requestId = asyncId;
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShard.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShard.java
new file mode 100644
index 00000000000..cb483dfd262
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShard.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static
org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static
org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static
org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonAdminParams.NUM_SUB_SHARDS;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_BY_PREFIX;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_FUZZ;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_KEY;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
+import static
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static org.apache.solr.common.params.CommonParams.TIMING;
+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.handler.api.V2ApiUtils.flattenMapWithPrefix;
+import static
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import jakarta.inject.Inject;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.SplitShardApi;
+import org.apache.solr.client.api.model.SplitShardRequestBody;
+import org.apache.solr.client.api.model.SplitShardResponse;
+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.CoreAdminParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for splitting an existing shard into multiple pieces.
+ *
+ * <p>This API (POST /v2/collections/collectionName/shards/shardName/split) is
analogous to the v1
+ * /admin/collections?action=SPLITSHARD command.
+ */
+public class SplitShard extends AdminAPIBase implements SplitShardApi {
+
+ @Inject
+ public SplitShard(
+ CoreContainer coreContainer,
+ SolrQueryRequest solrQueryRequest,
+ SolrQueryResponse solrQueryResponse) {
+ super(coreContainer, solrQueryRequest, solrQueryResponse);
+ }
+
+ @Override
+ @PermissionName(COLL_EDIT_PERM)
+ public SplitShardResponse splitShard(
+ String collectionName, String shardName, SplitShardRequestBody
requestBody) throws Exception {
+ final var response = instantiateJerseyResponse(SplitShardResponse.class);
+ fetchAndValidateZooKeeperAwareCoreContainer();
+ ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+ ensureRequiredParameterProvided(SHARD_ID_PROP, shardName);
+ recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+ if (requestBody == null) {
+ requestBody = new SplitShardRequestBody();
+ }
+ validateSplitOptions(requestBody);
+ return submitSplitMessage(response, collectionName, shardName,
requestBody);
+ }
+
+ /**
+ * The v1 form of split-by-key: the request names a collection and a split
key but no shard, so
+ * there is no shard path parameter to populate. The overseer resolves the
shard from the key.
+ */
+ private SplitShardResponse splitShardByKey(
+ String collectionName, SplitShardRequestBody requestBody) throws
Exception {
+ final var response = instantiateJerseyResponse(SplitShardResponse.class);
+ fetchAndValidateZooKeeperAwareCoreContainer();
+ ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+ recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+ validateSplitOptions(requestBody);
+ return submitSplitMessage(response, collectionName, null, requestBody);
+ }
+
+ private SplitShardResponse submitSplitMessage(
+ SplitShardResponse response,
+ String collectionName,
+ String shardName,
+ SplitShardRequestBody requestBody)
+ throws Exception {
+ final Map<String, Object> messageProps = new HashMap<>();
+ messageProps.put(COLLECTION_PROP, collectionName);
+ insertIfNotNull(messageProps, SHARD_ID_PROP, shardName);
+ insertIfNotNull(messageProps, "ranges", requestBody.ranges);
+ insertIfNotNull(messageProps, SPLIT_KEY, requestBody.splitKey);
+ insertIfNotNull(messageProps, "numSubShards", requestBody.numSubShards);
+ insertIfNotNull(messageProps, "splitFuzz", requestBody.splitFuzz);
+ insertIfNotNull(messageProps, "timing", requestBody.timing);
+ insertIfNotNull(messageProps, "splitByPrefix", requestBody.splitByPrefix);
+ insertIfNotNull(messageProps, "followAliases", requestBody.followAliases);
+ insertIfNotNull(messageProps, "splitMethod", requestBody.splitMethod);
+ insertIfNotNull(messageProps, "waitForFinalState",
requestBody.waitForFinalState);
+ insertIfNotNull(messageProps, CREATE_NODE_SET_PARAM,
requestBody.createNodeSet);
+ if (requestBody.coreProperties != null) {
+ flattenMapWithPrefix(requestBody.coreProperties, messageProps,
PROPERTY_PREFIX);
+ }
+
+ final var remoteResponse =
+ submitRemoteMessageAndHandleResponse(
+ response,
+ CollectionParams.CollectionAction.SPLITSHARD,
+ new ZkNodeProps(messageProps),
+ requestBody.async,
+ DEFAULT_COLLECTION_OP_TIMEOUT * 5);
+ final var timing = (NamedList<?>) remoteResponse.getResponse().get(TIMING);
+ if (timing != null) {
+ response.timing = new LinkedHashMap<>();
+ timing.forEach(response.timing::put);
+ }
+ return response;
+ }
+
+ public static SplitShardRequestBody createRequestBodyFromV1Params(SolrParams
params) {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.ranges = params.get(CoreAdminParams.RANGES);
+ requestBody.splitKey = params.get(SPLIT_KEY);
+ requestBody.numSubShards = params.getInt(NUM_SUB_SHARDS);
+ requestBody.splitFuzz = params.get(SPLIT_FUZZ);
+ requestBody.timing = params.getBool(TIMING);
+ requestBody.splitByPrefix = params.getBool(SPLIT_BY_PREFIX);
+ requestBody.followAliases = params.getBool(FOLLOW_ALIASES);
+ requestBody.splitMethod = params.get(SPLIT_METHOD);
+ requestBody.waitForFinalState = params.getBool(WAIT_FOR_FINAL_STATE);
+ requestBody.createNodeSet = params.get(CREATE_NODE_SET_PARAM);
+ final Map<String, String> coreProperties =
+ copyPrefixedPropertiesWithoutPrefix(params, new HashMap<>(),
PROPERTY_PREFIX);
+ if (!coreProperties.isEmpty()) {
+ requestBody.coreProperties = new HashMap<>(coreProperties);
+ }
+ // The v1 'async' parameter becomes the submission's async id: a delegated
operation
+ // returns no message map, so the v1 handler submits nothing itself.
+ requestBody.async = params.get(ASYNC);
+ return requestBody;
+ }
+
+ public static void invokeWithV1Params(
+ CoreContainer coreContainer,
+ SolrQueryRequest solrQueryRequest,
+ SolrQueryResponse solrQueryResponse)
+ throws Exception {
+ final var v1Params = solrQueryRequest.getParams();
+ final var collectionName = v1Params.required().get(COLLECTION_PROP);
+ final var shardName = v1Params.get(SHARD_ID_PROP);
+ final var requestBody = createRequestBodyFromV1Params(v1Params);
+ if (shardName == null && requestBody.splitKey == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "At least one of shard, or split.key should be specified.");
+ }
+ if (shardName != null && requestBody.splitKey != null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Only one of 'shard' or 'split.key' should be specified");
+ }
+
+ final var splitShardApi = new SplitShard(coreContainer, solrQueryRequest,
solrQueryResponse);
+ final SplitShardResponse response;
+ if (shardName != null) {
+ response = splitShardApi.splitShard(collectionName, shardName,
requestBody);
+ } else {
+ response = splitShardApi.splitShardByKey(collectionName, requestBody);
+ }
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(solrQueryResponse,
response);
+ }
+
+ private static void validateSplitOptions(SplitShardRequestBody requestBody) {
+ if (requestBody.splitKey != null && requestBody.ranges != null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Only one of 'ranges' or 'split.key' should be specified");
+ }
+ if (requestBody.numSubShards != null
+ && (requestBody.splitKey != null || requestBody.ranges != null)) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "numSubShards can not be specified with split.key or ranges
parameters");
+ }
+ if (requestBody.splitFuzz != null
+ && (requestBody.splitKey != null || requestBody.ranges != null)) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "fuzz can not be specified with split.key or ranges parameters");
+ }
+ }
+}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShardAPI.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShardAPI.java
deleted file mode 100644
index 873bfe3393a..00000000000
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SplitShardAPI.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
-import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
-import static
org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
-import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
-import static
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
-
-import java.util.HashMap;
-import java.util.Map;
-import org.apache.solr.api.Command;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.SplitShardPayload;
-import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CommonAdminParams;
-import org.apache.solr.common.util.StrUtils;
-import org.apache.solr.common.util.Utils;
-import org.apache.solr.handler.admin.CollectionsHandler;
-
-/**
- * V2 API for splitting an existing shard up into multiple pieces.
- *
- * <p>This API (POST /v2/collections/collectionName/shards {'split': {...}})
is analogous to the v1
- * /admin/collections?action=SPLITSHARD command.
- *
- * @see SplitShardPayload
- */
-@EndPoint(
- path = {"/c/{collection}/shards", "/collections/{collection}/shards"},
- method = POST,
- permission = COLL_EDIT_PERM)
-public class SplitShardAPI {
- private static final String V2_SPLIT_CMD = "split";
-
- private final CollectionsHandler collectionsHandler;
-
- public SplitShardAPI(CollectionsHandler collectionsHandler) {
- this.collectionsHandler = collectionsHandler;
- }
-
- @Command(name = V2_SPLIT_CMD)
- public void splitShard(PayloadObj<SplitShardPayload> obj) throws Exception {
- final SplitShardPayload v2Body = obj.get();
- final Map<String, Object> v1Params = Utils.convertToMap(v2Body, new
HashMap<>());
- v1Params.put(ACTION,
CollectionParams.CollectionAction.SPLITSHARD.toLower());
- v1Params.put(COLLECTION,
obj.getRequest().getPathTemplateValues().get(COLLECTION));
-
- if (StrUtils.isNotNullOrEmpty(v2Body.splitKey)) {
- v1Params.put(CommonAdminParams.SPLIT_KEY, v2Body.splitKey);
- }
- flattenMapWithPrefix(v2Body.coreProperties, v1Params, PROPERTY_PREFIX);
- collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(),
v1Params), obj.getResponse());
- }
-}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
index e581c574016..2761017c68c 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
@@ -94,18 +94,14 @@ public class TestApiFramework extends SolrTestCaseJ4 {
coreHandlers.put("/admin/ping", new PingRequestHandler());
Map<String, String> parts = new HashMap<>();
- String fullPath = "/collections/hello/shards";
+ String fullPath = "/collections/hello";
Api api = V2HttpCall.getApiInfo(containerHandlers, fullPath, "POST",
fullPath, parts);
assertNotNull(api);
assertEquals("hello", parts.get("collection"));
parts = new HashMap<>();
- api =
- V2HttpCall.getApiInfo(containerHandlers, "/collections/hello/shards",
"POST", null, parts);
- assertConditions(api.getSpec(), Map.of("/methods[0]", "POST",
"/commands/split", NOT_NULL));
-
- parts = new HashMap<>();
- api = V2HttpCall.getApiInfo(containerHandlers, "/collections/hello",
"POST", null, parts);
+ api = V2HttpCall.getApiInfo(containerHandlers, fullPath, "POST", null,
parts);
+ assertNotNull(api);
assertConditions(api.getSpec(), Map.of("/methods[0]", "POST",
"/commands/modify", NOT_NULL));
assertEquals("hello", parts.get("collection"));
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
index 99828d971a0..6c7762f4335 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
@@ -87,20 +87,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
apiBag.registerObject(clusterAPI.commands);
}
- compareOutput(
- apiBag,
- "/collections/collName/shards",
- POST,
- "{split:{shard:shard1, ranges: '0-1f4,1f5-3e8,3e9-5dc', coreProperties
: {prop1:prop1Val, prop2:prop2Val} }}",
- "{collection: collName , shard : shard1, ranges
:'0-1f4,1f5-3e8,3e9-5dc', operation : splitshard, property.prop1:prop1Val,
property.prop2: prop2Val}");
-
- compareOutput(
- apiBag,
- "/collections/collName/shards",
- POST,
- "{split:{ splitKey:id12345, coreProperties : {prop1:prop1Val,
prop2:prop2Val} }}",
- "{collection: collName , split.key : id12345 , operation : splitshard,
property.prop1:prop1Val, property.prop2: prop2Val}");
-
compareOutput(
apiBag,
"/collections/coll1",
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/SplitShardAPITest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/SplitShardAPITest.java
new file mode 100644
index 00000000000..406f9b7fbbd
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/SplitShardAPITest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import java.util.Map;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ShardsApi;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.cloud.Slice;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Test cases for the v2 {@link SplitShard} API, driven through the generated
SolrJ client. */
+public class SplitShardAPITest extends SolrCloudTestCase {
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ configureCluster(2).addConfig("conf",
configset("cloud-minimal")).configure();
+ }
+
+ @Test
+ public void testSplitShard() throws Exception {
+ final String collection = "splitShardAPITestColl";
+ CollectionAdminRequest.createCollection(collection, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collection, 1, 1);
+
+ final ShardsApi.SplitShard splitRequest = new
ShardsApi.SplitShard(collection, "shard1");
+ splitRequest.setNumSubShards(2);
+ splitRequest.setTiming(true);
+ final var response = splitRequest.process(cluster.getSolrClient());
+ assertEquals(0, response.responseHeader.status);
+ assertNotNull(response.timing);
+ assertTrue(response.timing.get("time") instanceof Number);
+ assertTrue(response.timing.get("splitParentCore") instanceof Map<?, ?>);
+ assertTrue(((Map<?, ?>)
response.timing.get("splitParentCore")).get("time") instanceof Number);
+
+ waitForState(
+ "Expected sub-shards shard1_0 and shard1_1 to be active",
+ collection,
+ c -> c != null && isActive(c.getSlice("shard1_0")) &&
isActive(c.getSlice("shard1_1")));
+ }
+
+ @Test
+ public void testSplitShardWithRanges() throws Exception {
+ final String collection = "splitShardRangesTestColl";
+ CollectionAdminRequest.createCollection(collection, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collection, 1, 1);
+
+ // An uneven split of the parent range, so the sub-shard ranges can only
match if the
+ // 'ranges' value reached the split command unchanged.
+ final ShardsApi.SplitShard splitRequest = new
ShardsApi.SplitShard(collection, "shard1");
+ splitRequest.setRanges("80000000-bfffffff,c0000000-7fffffff");
+ assertEquals(0,
splitRequest.process(cluster.getSolrClient()).responseHeader.status);
+
+ waitForState(
+ "Expected sub-shards shard1_0 and shard1_1 to be active",
+ collection,
+ c -> c != null && isActive(c.getSlice("shard1_0")) &&
isActive(c.getSlice("shard1_1")));
+
+ final var docCollection = getCollectionState(collection);
+ assertEquals(0x80000000,
docCollection.getSlice("shard1_0").getRange().min);
+ assertEquals(0xbfffffff,
docCollection.getSlice("shard1_0").getRange().max);
+ assertEquals(0xc0000000,
docCollection.getSlice("shard1_1").getRange().min);
+ assertEquals(0x7fffffff,
docCollection.getSlice("shard1_1").getRange().max);
+ }
+
+ @Test
+ public void testSplitShardAsync() throws Exception {
+ final String collection = "splitShardAsyncTestColl";
+ CollectionAdminRequest.createCollection(collection, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collection, 1, 1);
+
+ final ShardsApi.SplitShard splitRequest = new
ShardsApi.SplitShard(collection, "shard1");
+ splitRequest.setNumSubShards(2);
+ splitRequest.setAsync("splitShardAsyncTestId");
+ final var response = splitRequest.process(cluster.getSolrClient());
+ assertEquals(0, response.responseHeader.status);
+ assertEquals("splitShardAsyncTestId", response.requestId);
+
+ waitForState(
+ "Expected sub-shards shard1_0 and shard1_1 to be active",
+ collection,
+ c -> c != null && isActive(c.getSlice("shard1_0")) &&
isActive(c.getSlice("shard1_1")));
+ }
+
+ @Test
+ public void testRejectsConflictingSplitOptions() throws Exception {
+ final String collection = "splitShardConflictingOptionsTestColl";
+ CollectionAdminRequest.createCollection(collection, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collection, 1, 1);
+
+ final ShardsApi.SplitShard splitRequest = new
ShardsApi.SplitShard(collection, "shard1");
+ splitRequest.setRanges("80000000-bfffffff,c0000000-7fffffff");
+ splitRequest.setNumSubShards(3);
+
+ final RemoteSolrException exception =
+ expectThrows(
+ RemoteSolrException.class, () ->
splitRequest.process(cluster.getSolrClient()));
+ assertEquals(400, exception.code());
+ assertTrue(
+ exception
+ .getMessage()
+ .contains("numSubShards can not be specified with split.key or
ranges"));
+ }
+
+ @Test
+ public void testSplitMissingCollectionThrows() {
+ final ShardsApi.SplitShard splitRequest =
+ new ShardsApi.SplitShard("missingCollection", "shard1");
+ final RemoteSolrException ex =
+ expectThrows(
+ RemoteSolrException.class, () ->
splitRequest.process(cluster.getSolrClient()));
+ assertTrue("Expected an error status, got " + ex.code(), ex.code() >= 400);
+ }
+
+ private static boolean isActive(Slice slice) {
+ return slice != null && slice.getState() == Slice.State.ACTIVE;
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/SplitShardMessageMappingTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/SplitShardMessageMappingTest.java
new file mode 100644
index 00000000000..74dd6527271
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/SplitShardMessageMappingTest.java
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static
org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static
org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.NUM_SUB_SHARDS;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_BY_PREFIX;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_FUZZ;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_KEY;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
+import static
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static org.apache.solr.common.params.CommonParams.TIMING;
+import static org.apache.solr.common.params.CoreAdminParams.RANGES;
+import static
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import org.apache.solr.client.api.model.SplitShardRequestBody;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.cloud.OverseerSolrResponse;
+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.apache.solr.common.util.NamedList;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for the {@link SplitShard} JAX-RS resource: every {@link
SplitShardRequestBody} field
+ * must reach the overseer message (or, for 'async', the submission's async
id) under the name the
+ * v1 SPLITSHARD command reads.
+ *
+ * <p>Mutually exclusive split options are exercised separately.
+ */
+public class SplitShardMessageMappingTest extends MockV2APITest {
+
+ private SplitShard api;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
+
+ api = new SplitShard(mockCoreContainer, mockQueryRequest, queryResponse);
+ }
+
+ @Test
+ public void testSplitShardCommonProperties() throws Exception {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.numSubShards = 3;
+ requestBody.splitFuzz = "0.1";
+ requestBody.timing = true;
+ requestBody.splitByPrefix = false;
+ requestBody.followAliases = true;
+ requestBody.splitMethod = "rewrite";
+ requestBody.createNodeSet = "node1:8983_solr,node2:8983_solr";
+ requestBody.coreProperties = Map.of("foo", "foo1", "bar", "bar1");
+ requestBody.async = "some_async_id";
+ requestBody.waitForFinalState = true;
+
+ api.splitShard("collName", "shard1", requestBody);
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ "some_async_id",
+ message -> {
+ assertEquals(12, message.size());
+ assertEquals("collName", message.get(COLLECTION_PROP));
+ assertEquals("shard1", message.get(SHARD_ID_PROP));
+ assertEquals(3, message.get(NUM_SUB_SHARDS));
+ assertEquals("0.1", message.get(SPLIT_FUZZ));
+ assertEquals(Boolean.TRUE, message.get(TIMING));
+ assertEquals(Boolean.FALSE, message.get(SPLIT_BY_PREFIX));
+ assertEquals(Boolean.TRUE, message.get(FOLLOW_ALIASES));
+ assertEquals("rewrite", message.get(SPLIT_METHOD));
+ assertEquals("node1:8983_solr,node2:8983_solr",
message.get(CREATE_NODE_SET_PARAM));
+ assertEquals(Boolean.TRUE, message.get(WAIT_FOR_FINAL_STATE));
+ assertEquals("foo1", message.get("property.foo"));
+ assertEquals("bar1", message.get("property.bar"));
+ });
+ }
+
+ @Test
+ public void testSplitShardWithRanges() throws Exception {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.ranges = "80000000-bfffffff,c0000000-7fffffff";
+
+ api.splitShard("collName", "shard1", requestBody);
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ message -> {
+ assertEquals(3, message.size());
+ assertEquals(requestBody.ranges, message.get(RANGES));
+ });
+ }
+
+ @Test
+ public void testSplitShardWithSplitKey() throws Exception {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.splitKey = "someKey!";
+ requestBody.splitByPrefix = true;
+
+ api.splitShard("collName", "shard1", requestBody);
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ message -> {
+ assertEquals(4, message.size());
+ assertEquals(requestBody.splitKey, message.get(SPLIT_KEY));
+ assertEquals(Boolean.TRUE, message.get(SPLIT_BY_PREFIX));
+ });
+ }
+
+ @Test
+ public void testSplitShardUsesExtendedTimeout() throws Exception {
+ api.splitShard("collName", "shard1", new SplitShardRequestBody());
+
+ verify(mockCommandRunner)
+ .runCollectionCommand(any(), any(), eq(DEFAULT_COLLECTION_OP_TIMEOUT *
5));
+ }
+
+ @Test
+ public void testOtherCommandsRetainDefaultTimeout() throws Exception {
+ api.submitRemoteMessageAndHandleResponse(
+ new SubResponseAccumulatingJerseyResponse(),
+ CollectionParams.CollectionAction.CREATESHARD,
+ new ZkNodeProps(Map.of(COLLECTION_PROP, "collName", SHARD_ID_PROP,
"shard2")),
+ null);
+
+ verify(mockCommandRunner).runCollectionCommand(any(), any(),
eq(DEFAULT_COLLECTION_OP_TIMEOUT));
+ }
+
+ @Test
+ public void testSplitShardPreservesTimingAndSubResponses() throws Exception {
+ final var phaseTiming = new NamedList<Object>();
+ phaseTiming.add("time", 7D);
+ final var timing = new NamedList<Object>();
+ timing.add("time", 12D);
+ timing.add("split", phaseTiming);
+ final var success = Map.of("node1", Map.of("status", 0));
+ final var remoteResponse = new NamedList<Object>();
+ remoteResponse.add(TIMING, timing);
+ remoteResponse.add("success", success);
+ remoteResponse.add("warning", "some warning");
+ when(mockCommandRunner.runCollectionCommand(any(), any(), anyLong()))
+ .thenReturn(new OverseerSolrResponse(remoteResponse));
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.timing = true;
+
+ final var response = api.splitShard("collName", "shard1", requestBody);
+
+ assertNotNull(response.timing);
+ assertEquals(Double.valueOf(12D), response.timing.get("time"));
+ assertSame(phaseTiming, response.timing.get("split"));
+ assertEquals(success, response.successfulSubResponsesByNodeName);
+ assertEquals("some warning", response.warning);
+ }
+
+ @Test
+ public void testSplitShardOmitsUnrequestedTiming() throws Exception {
+ final var response = api.splitShard("collName", "shard1", new
SplitShardRequestBody());
+
+ assertNull(response.timing);
+ }
+
+ @Test
+ public void testRejectsRangesAndSplitKey() {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.ranges = "80000000-bfffffff,c0000000-7fffffff";
+ requestBody.splitKey = "someKey!";
+
+ assertRejectedSplit(requestBody, "Only one of 'ranges' or 'split.key'
should be specified");
+ }
+
+ @Test
+ public void testRejectsNumSubShardsAndRanges() {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.ranges = "80000000-bfffffff,c0000000-7fffffff";
+ requestBody.numSubShards = 3;
+
+ assertRejectedSplit(
+ requestBody, "numSubShards can not be specified with split.key or
ranges parameters");
+ }
+
+ @Test
+ public void testRejectsNumSubShardsAndSplitKey() {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.splitKey = "someKey!";
+ requestBody.numSubShards = 3;
+
+ assertRejectedSplit(
+ requestBody, "numSubShards can not be specified with split.key or
ranges parameters");
+ }
+
+ @Test
+ public void testRejectsSplitFuzzAndRanges() {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.ranges = "80000000-bfffffff,c0000000-7fffffff";
+ requestBody.splitFuzz = "0.1";
+
+ assertRejectedSplit(
+ requestBody, "fuzz can not be specified with split.key or ranges
parameters");
+ }
+
+ @Test
+ public void testRejectsSplitFuzzAndSplitKey() {
+ final var requestBody = new SplitShardRequestBody();
+ requestBody.splitKey = "someKey!";
+ requestBody.splitFuzz = "0.1";
+
+ assertRejectedSplit(
+ requestBody, "fuzz can not be specified with split.key or ranges
parameters");
+ }
+
+ private void assertRejectedSplit(SplitShardRequestBody requestBody, String
expectedMessage) {
+ final SolrException exception =
+ expectThrows(SolrException.class, () -> api.splitShard("collName",
"shard1", requestBody));
+
+ assertEquals(400, exception.code());
+ assertEquals(expectedMessage, exception.getMessage());
+ verifyNoInteractions(mockCommandRunner);
+ }
+
+ @Test
+ public void testSplitShardWithEmptyBody() throws Exception {
+ api.splitShard("collName", "shard1", new SplitShardRequestBody());
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ message -> {
+ assertEquals(2, message.size());
+ assertEquals("collName", message.get(COLLECTION_PROP));
+ assertEquals("shard1", message.get(SHARD_ID_PROP));
+ });
+ }
+
+ @Test
+ public void testSplitShardWithNullBody() throws Exception {
+ api.splitShard("collName", "shard1", null);
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ message -> {
+ assertEquals(2, message.size());
+ assertEquals("collName", message.get(COLLECTION_PROP));
+ assertEquals("shard1", message.get(SHARD_ID_PROP));
+ });
+ }
+
+ @Test
+ public void testCreateRequestBodyFromV1Params() {
+ final var v1Params = new ModifiableSolrParams();
+ v1Params.set(RANGES, "80000000-bfffffff,c0000000-7fffffff");
+ v1Params.set(NUM_SUB_SHARDS, "3");
+ v1Params.set(SPLIT_FUZZ, "0.1");
+ v1Params.set(TIMING, "true");
+ v1Params.set(SPLIT_BY_PREFIX, "true");
+ v1Params.set(FOLLOW_ALIASES, "true");
+ v1Params.set(SPLIT_METHOD, "rewrite");
+ v1Params.set(CREATE_NODE_SET_PARAM, "node1:8983_solr,node2:8983_solr");
+ v1Params.set(WAIT_FOR_FINAL_STATE, "true");
+ v1Params.set("property.foo", "foo1");
+ v1Params.set("async", "v1AsyncId");
+
+ final var requestBody = SplitShard.createRequestBodyFromV1Params(v1Params);
+
+ assertEquals("80000000-bfffffff,c0000000-7fffffff", requestBody.ranges);
+ assertEquals(Integer.valueOf(3), requestBody.numSubShards);
+ assertEquals("0.1", requestBody.splitFuzz);
+ assertEquals(Boolean.TRUE, requestBody.timing);
+ assertEquals(Boolean.TRUE, requestBody.splitByPrefix);
+ assertEquals(Boolean.TRUE, requestBody.followAliases);
+ assertEquals("rewrite", requestBody.splitMethod);
+ assertEquals("node1:8983_solr,node2:8983_solr", requestBody.createNodeSet);
+ assertEquals(Boolean.TRUE, requestBody.waitForFinalState);
+ assertEquals(Map.of("foo", "foo1"), requestBody.coreProperties);
+ // The v1 'async' parameter becomes the request body's 'async' field: a
delegated
+ // operation returns no message map, so the v1 handler submits nothing
itself and
+ // the resource's submission carries the async id.
+ assertEquals("v1AsyncId", requestBody.async);
+ }
+
+ @Test
+ public void testInvokeWithV1ParamsShardPath() throws Exception {
+ final var v1Params = new ModifiableSolrParams();
+ v1Params.set(COLLECTION_PROP, "collName");
+ v1Params.set(SHARD_ID_PROP, "shard1");
+ v1Params.set(NUM_SUB_SHARDS, "3");
+ v1Params.set(CREATE_NODE_SET_PARAM, "node1:8983_solr,node2:8983_solr");
+ v1Params.set("property.foo", "foo1");
+ v1Params.set("async", "v1AsyncId");
+ when(mockQueryRequest.getParams()).thenReturn(v1Params);
+
+ SplitShard.invokeWithV1Params(mockCoreContainer, mockQueryRequest,
queryResponse);
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ "v1AsyncId",
+ message -> {
+ assertEquals(5, message.size());
+ assertEquals("collName", message.get(COLLECTION_PROP));
+ assertEquals("shard1", message.get(SHARD_ID_PROP));
+ assertEquals(3, message.get(NUM_SUB_SHARDS));
+ assertEquals("node1:8983_solr,node2:8983_solr",
message.get(CREATE_NODE_SET_PARAM));
+ assertEquals("foo1", message.get("property.foo"));
+ });
+ }
+
+ @Test
+ public void testInvokeWithV1ParamsSplitKeyOnly() throws Exception {
+ final var v1Params = new ModifiableSolrParams();
+ v1Params.set(COLLECTION_PROP, "collName");
+ v1Params.set(SPLIT_KEY, "someKey!");
+ when(mockQueryRequest.getParams()).thenReturn(v1Params);
+
+ SplitShard.invokeWithV1Params(mockCoreContainer, mockQueryRequest,
queryResponse);
+
+ validateRunCommand(
+ CollectionParams.CollectionAction.SPLITSHARD,
+ message -> {
+ assertEquals(2, message.size());
+ assertEquals("collName", message.get(COLLECTION_PROP));
+ assertEquals("someKey!", message.get(SPLIT_KEY));
+ assertFalse(message.containsKey(SHARD_ID_PROP));
+ });
+ }
+
+ @Test
+ public void testInvokeWithV1ParamsRejectsShardAndSplitKey() {
+ final var v1Params = new ModifiableSolrParams();
+ v1Params.set(COLLECTION_PROP, "collName");
+ v1Params.set(SHARD_ID_PROP, "shard1");
+ v1Params.set(SPLIT_KEY, "someKey!");
+ when(mockQueryRequest.getParams()).thenReturn(v1Params);
+
+ final SolrException exception =
+ expectThrows(
+ SolrException.class,
+ () ->
+ SplitShard.invokeWithV1Params(mockCoreContainer,
mockQueryRequest, queryResponse));
+
+ assertEquals(400, exception.code());
+ assertEquals("Only one of 'shard' or 'split.key' should be specified",
exception.getMessage());
+ verifyNoInteractions(mockCommandRunner);
+ }
+
+ @Test
+ public void testInvokeWithV1ParamsRejectsNeitherShardNorSplitKey() {
+ final var v1Params = new ModifiableSolrParams();
+ v1Params.set(COLLECTION_PROP, "collName");
+ when(mockQueryRequest.getParams()).thenReturn(v1Params);
+
+ final SolrException exception =
+ expectThrows(
+ SolrException.class,
+ () ->
+ SplitShard.invokeWithV1Params(mockCoreContainer,
mockQueryRequest, queryResponse));
+
+ assertEquals(400, exception.code());
+ assertEquals(
+ "At least one of shard, or split.key should be specified.",
exception.getMessage());
+ verifyNoInteractions(mockCommandRunner);
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
deleted file mode 100644
index 9731922de46..00000000000
---
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.solr.handler.admin.api;
-
-import static org.apache.solr.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.CommonAdminParams.NUM_SUB_SHARDS;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_BY_PREFIX;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_FUZZ;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_KEY;
-import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
-import static
org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.common.params.CommonParams.TIMING;
-
-import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CoreAdminParams;
-import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.handler.admin.CollectionsHandler;
-import org.apache.solr.handler.admin.V2ApiMappingTest;
-import org.junit.Test;
-
-/**
- * Unit tests for the V2 APIs that use the /c/{collection}/shards or
/c/{collection}/shards/{shard}
- * paths.
- *
- * <p>Note that the V2 requests made by these tests are not necessarily
semantically valid. They
- * shouldn't be taken as examples. In several instances, mutually exclusive
JSON parameters are
- * provided. This is done to exercise conversion of all parameters, even if
particular combinations
- * are never expected in the same request.
- */
-public class V2ShardsAPIMappingTest extends
V2ApiMappingTest<CollectionsHandler> {
-
- @Override
- public void populateApiBag() {
- final CollectionsHandler collectionsHandler = getRequestHandler();
- apiBag.registerObject(new SplitShardAPI(collectionsHandler));
- }
-
- @Override
- public CollectionsHandler createUnderlyingRequestHandler() {
- return createMock(CollectionsHandler.class);
- }
-
- @Override
- public boolean isCoreSpecific() {
- return false;
- }
-
- @Test
- public void testSplitShardAllProperties() throws Exception {
- final SolrParams v1Params =
- captureConvertedV1Params(
- "/collections/collName/shards",
- "POST",
- "{ 'split': {"
- + "'shard': 'shard1', "
- + "'ranges': 'someRangeValues', "
- + "'splitKey': 'someSplitKey', "
- + "'numSubShards': 123, "
- + "'splitFuzz': 'some_fuzz_value', "
- + "'timing': true, "
- + "'splitByPrefix': true, "
- + "'followAliases': true, "
- + "'splitMethod': 'rewrite', "
- + "'async': 'some_async_id', "
- + "'waitForFinalState': true, "
- + "'coreProperties': {"
- + " 'foo': 'foo1', "
- + " 'bar': 'bar1', "
- + "}}}");
-
- assertEquals(CollectionParams.CollectionAction.SPLITSHARD.lowerName,
v1Params.get(ACTION));
- assertEquals("collName", v1Params.get(COLLECTION));
- assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
- assertEquals("someRangeValues", v1Params.get(CoreAdminParams.RANGES));
- assertEquals("someSplitKey", v1Params.get(SPLIT_KEY));
- assertEquals(123, v1Params.getPrimitiveInt(NUM_SUB_SHARDS));
- assertEquals("some_fuzz_value", v1Params.get(SPLIT_FUZZ));
- assertTrue(v1Params.getPrimitiveBool(TIMING));
- assertTrue(v1Params.getPrimitiveBool(SPLIT_BY_PREFIX));
- assertTrue(v1Params.getPrimitiveBool(FOLLOW_ALIASES));
- assertEquals("rewrite", v1Params.get(SPLIT_METHOD));
- assertEquals("some_async_id", v1Params.get(ASYNC));
- assertTrue(v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
- assertEquals("foo1", v1Params.get("property.foo"));
- assertEquals("bar1", v1Params.get("property.bar"));
- }
-}