You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by is...@apache.org on 2023/10/24 00:37:31 UTC
[lucene-solr] branch branch_8_11 updated: SOLR-15694, 15715: Node roles and dedicated query coordinator nodes
This is an automated email from the ASF dual-hosted git repository.
ishan pushed a commit to branch branch_8_11
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git
The following commit(s) were added to refs/heads/branch_8_11 by this push:
new 2da4332072f SOLR-15694, 15715: Node roles and dedicated query coordinator nodes
2da4332072f is described below
commit 2da4332072ffffb0581ef60d11de5b0d157452ac
Author: Ishan Chattopadhyaya <is...@searchscale.com>
AuthorDate: Tue Oct 24 06:07:05 2023 +0530
SOLR-15694, 15715: Node roles and dedicated query coordinator nodes
---
solr/CHANGES.txt | 5 +
.../solr/handler/component/AnalyticsComponent.java | 2 +-
.../LTRFeatureLoggerTransformerFactory.java | 2 +-
.../apache/solr/ltr/search/LTRQParserPlugin.java | 2 +-
.../apache/solr/api/CoordinatorV2HttpSolrCall.java | 58 ++
.../apache/solr/cloud/api/collections/Assign.java | 29 +-
.../cloud/api/collections/CategoryRoutedAlias.java | 2 +-
.../cloud/api/collections/CreateCollectionCmd.java | 7 +-
.../solr/cloud/api/collections/RestoreCmd.java | 8 +-
.../solr/cloud/api/collections/RoutedAlias.java | 2 +-
.../cloud/api/collections/TimeRoutedAlias.java | 2 +-
.../java/org/apache/solr/core/CoreContainer.java | 1 +
.../src/java/org/apache/solr/core/NodeRoles.java | 152 ++++
.../java/org/apache/solr/handler/ClusterAPI.java | 129 +++
.../apache/solr/handler/RequestHandlerBase.java | 4 +-
.../org/apache/solr/handler/SolrConfigHandler.java | 10 +-
.../solr/handler/admin/ShowFileRequestHandler.java | 4 +-
.../solr/handler/component/HttpShardHandler.java | 4 +-
.../handler/component/HttpShardHandlerFactory.java | 2 +-
.../solr/handler/component/QueryComponent.java | 2 +-
.../handler/component/RealTimeGetComponent.java | 10 +-
.../solr/handler/component/SearchHandler.java | 4 +-
.../solr/request/DelegatingSolrQueryRequest.java | 160 ++++
.../java/org/apache/solr/request/SimpleFacets.java | 2 +-
.../org/apache/solr/request/SolrQueryRequest.java | 14 +-
.../response/transform/CoreAugmenterFactory.java} | 21 +-
.../response/transform/TransformerFactory.java | 2 +
.../org/apache/solr/search/JoinQParserPlugin.java | 2 +-
.../solr/search/join/ScoreJoinQParserPlugin.java | 4 +-
.../solr/servlet/CoordinatorHttpSolrCall.java | 352 ++++++++
.../java/org/apache/solr/servlet/HttpSolrCall.java | 9 +
.../apache/solr/servlet/SolrDispatchFilter.java | 35 +-
.../src/java/org/apache/solr/update/UpdateLog.java | 2 +-
.../DistributedUpdateProcessorFactory.java | 2 +-
.../processor/RoutedAliasUpdateProcessor.java | 10 +-
.../solr/configsets/cache-control/conf/schema.xml | 27 +
.../configsets/cache-control/conf/solrconfig.xml | 54 ++
.../solr/configsets/conf3/conf/schema.xml | 43 +
.../solr/configsets/conf3/conf/solrconfig.xml | 68 ++
.../solr/common/cloud/ZkStateReaderAccessor.java | 5 +-
.../apache/solr/search/TestCoordinatorRole.java | 889 +++++++++++++++++++++
.../org/apache/solr/client/solrj/SolrRequest.java | 13 +-
.../client/solrj/impl/BaseCloudSolrClient.java | 15 +
.../apache/solr/common/cloud/ZkStateReader.java | 10 +
.../impl/CloudHttp2SolrClientBadInputTest.java | 15 -
.../solrj/impl/CloudSolrClientBadInputTest.java | 15 -
...oncurrentUpdateHttp2SolrClientBadInputTest.java | 14 -
.../ConcurrentUpdateSolrClientBadInputTest.java | 15 -
.../solrj/impl/HttpSolrClientBadInputTest.java | 13 -
.../solrj/impl/LBHttpSolrClientBadInputTest.java | 15 -
.../src/java/org/apache/solr/SolrTestCaseJ4.java | 14 +
51 files changed, 2129 insertions(+), 152 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index dc431f2f62f..fbe9865e5d0 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -8,6 +8,11 @@ https://github.com/apache/solr/blob/main/solr/solr-ref-guide/src/solr-upgrade-no
Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release.
+New Features
+---------------------
+
+* SOLR-15694, 15715: Node roles and dedicated query coordinator nodes (Hitesh Khamesra, Ishan Chattopadhyaya, noble)
+
Bug Fixes
---------------------
* SOLR-16429: Add AWS STS support in s3-repository (Josh Souza via Houston Putman and Kevin Risden)
diff --git a/solr/contrib/analytics/src/java/org/apache/solr/handler/component/AnalyticsComponent.java b/solr/contrib/analytics/src/java/org/apache/solr/handler/component/AnalyticsComponent.java
index 5248181b76b..4017b6b0571 100644
--- a/solr/contrib/analytics/src/java/org/apache/solr/handler/component/AnalyticsComponent.java
+++ b/solr/contrib/analytics/src/java/org/apache/solr/handler/component/AnalyticsComponent.java
@@ -109,7 +109,7 @@ public class AnalyticsComponent extends SearchComponent {
// Send out a request to each shard and merge the responses into our AnalyticsRequestManager
reqManager.shardStream.sendRequests(rb.req.getCore().getCoreDescriptor().getCollectionName(),
- rb.req.getCore().getCoreContainer().getZkController().getZkServerAddress());
+ rb.req.getCoreContainer().getZkController().getZkServerAddress());
reqManager.sendShards = false;
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/response/transform/LTRFeatureLoggerTransformerFactory.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/response/transform/LTRFeatureLoggerTransformerFactory.java
index 96bd795ef3a..8f2aaa5796c 100644
--- a/solr/contrib/ltr/src/java/org/apache/solr/ltr/response/transform/LTRFeatureLoggerTransformerFactory.java
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/response/transform/LTRFeatureLoggerTransformerFactory.java
@@ -217,7 +217,7 @@ public class LTRFeatureLoggerTransformerFactory extends TransformerFactory {
}
leafContexts = searcher.getTopReaderContext().leaves();
if (threadManager != null) {
- threadManager.setExecutor(context.getRequest().getCore().getCoreContainer().getUpdateShardHandler().getUpdateExecutor());
+ threadManager.setExecutor(context.getRequest().getCoreContainer().getUpdateShardHandler().getUpdateExecutor());
}
rerankingQueriesFromContext = SolrQueryRequestContextUtils.getScoringQueries(req);
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
index 0bc0b678c2b..39e4828649d 100644
--- a/solr/contrib/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
@@ -153,7 +153,7 @@ public class LTRQParserPlugin extends QParserPlugin implements ResourceLoaderAwa
@Override
public Query parse() throws SyntaxError {
if (threadManager != null) {
- threadManager.setExecutor(req.getCore().getCoreContainer().getUpdateShardHandler().getUpdateExecutor());
+ threadManager.setExecutor(req.getCoreContainer().getUpdateShardHandler().getUpdateExecutor());
}
// ReRanking Model
final String[] modelNames = localParams.getParams(LTRQParserPlugin.MODEL);
diff --git a/solr/core/src/java/org/apache/solr/api/CoordinatorV2HttpSolrCall.java b/solr/core/src/java/org/apache/solr/api/CoordinatorV2HttpSolrCall.java
new file mode 100644
index 00000000000..f29d2407022
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/CoordinatorV2HttpSolrCall.java
@@ -0,0 +1,58 @@
+/*
+ * 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.api;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.servlet.CoordinatorHttpSolrCall;
+import org.apache.solr.servlet.SolrDispatchFilter;
+
+public class CoordinatorV2HttpSolrCall extends V2HttpCall {
+ private String collectionName;
+ CoordinatorHttpSolrCall.Factory factory;
+
+ public CoordinatorV2HttpSolrCall(
+ CoordinatorHttpSolrCall.Factory factory,
+ SolrDispatchFilter solrDispatchFilter,
+ CoreContainer cc,
+ HttpServletRequest request,
+ HttpServletResponse response,
+ boolean retry) {
+ super(solrDispatchFilter, cc, request, response, retry);
+ this.factory = factory;
+ }
+
+ @Override
+ protected SolrCore getCoreByCollection(String collectionName, boolean isPreferLeader) {
+ this.collectionName = collectionName;
+ SolrCore core = super.getCoreByCollection(collectionName, isPreferLeader);
+ if (core != null) return core;
+ if (!path.endsWith("/select")) return null;
+ return CoordinatorHttpSolrCall.getCore(factory, this, collectionName, isPreferLeader);
+ }
+
+ @Override
+ protected void init() throws Exception {
+ super.init();
+ if (action == SolrDispatchFilter.Action.PROCESS && core != null) {
+ solrReq = CoordinatorHttpSolrCall.wrappedReq(solrReq, collectionName, this);
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
index 138f5afc34e..8728cd3fab7 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/Assign.java
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -55,6 +56,8 @@ import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CollectionAdminParams;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.NodeRoles;
+import org.apache.solr.handler.ClusterAPI;
import org.apache.solr.util.NumberUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
@@ -233,7 +236,12 @@ public class Assign {
return false;
}
- public static List<String> getLiveOrLiveAndCreateNodeSetList(final Set<String> liveNodes, final ZkNodeProps message, final Random random) {
+ public static List<String> getLiveOrLiveAndCreateNodeSetList(
+ final Set<String> liveNodes,
+ final ZkNodeProps message,
+ final Random random,
+ DistribStateManager zk) {
+
List<String> nodeList;
final String createNodeSetStr = message.getStr(CREATE_NODE_SET);
final List<String> createNodeList = (createNodeSetStr == null) ? null :
@@ -248,13 +256,28 @@ public class Assign {
Collections.shuffle(nodeList, random);
}
} else {
- nodeList = new ArrayList<>(liveNodes);
+ nodeList = new ArrayList<>(filterNonDataNodes(zk, liveNodes));
Collections.shuffle(nodeList, random);
}
return nodeList;
}
-
+ public static Collection<String> filterNonDataNodes(
+ DistribStateManager zk, Collection<String> liveNodes) {
+ try {
+ List<String> noData = ClusterAPI.getNodesByRole(NodeRoles.Role.DATA, NodeRoles.MODE_OFF, zk);
+ if (noData.isEmpty()) {
+ return liveNodes;
+ } else {
+ liveNodes = new HashSet<>(liveNodes);
+ liveNodes.removeAll(noData);
+ return liveNodes;
+ }
+ } catch (Exception e) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR, "Error fetching roles from Zookeeper", e);
+ }
+ }
/**
* <b>Note:</b> where possible, the {@link #usePolicyFramework(DocCollection, SolrCloudManager)} method should
* be used instead of this method
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CategoryRoutedAlias.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CategoryRoutedAlias.java
index 0ba96c39716..24a2b4451c0 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CategoryRoutedAlias.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CategoryRoutedAlias.java
@@ -123,7 +123,7 @@ public class CategoryRoutedAlias extends RoutedAlias {
@Override
public void validateRouteValue(AddUpdateCommand cmd) throws SolrException {
if (this.aliases == null) {
- updateParsedCollectionAliases(cmd.getReq().getCore().getCoreContainer().getZkController().zkStateReader, false);
+ updateParsedCollectionAliases(cmd.getReq().getCoreContainer().getZkController().zkStateReader, false);
}
Object fieldValue = cmd.getSolrInputDocument().getFieldValue(getRouteField());
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 79ebfed5720..d9375ae4685 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
@@ -459,7 +459,12 @@ public class CreateCollectionCmd implements OverseerCollectionMessageHandler.Cmd
// but (for now) require that each core goes on a distinct node.
List<ReplicaPosition> replicaPositions;
- List<String> nodeList = Assign.getLiveOrLiveAndCreateNodeSetList(clusterState.getLiveNodes(), message, OverseerCollectionMessageHandler.RANDOM);
+ List<String> nodeList =
+ Assign.getLiveOrLiveAndCreateNodeSetList(
+ clusterState.getLiveNodes(),
+ message,
+ OverseerCollectionMessageHandler.RANDOM,
+ cloudManager.getDistribStateManager());
if (nodeList.isEmpty()) {
log.warn("It is unusual to create a collection ({}) without cores.", collectionName);
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 987d0d741be..df082278c73 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
@@ -189,8 +189,12 @@ public class RestoreCmd implements OverseerCollectionMessageHandler.Cmd {
this.backupCollectionState = this.backupManager.readCollectionState(this.backupCollection);
this.shardHandler = ocmh.shardHandlerFactory.getShardHandler();
- this.nodeList = Assign.getLiveOrLiveAndCreateNodeSetList(
- zkStateReader.getClusterState().getLiveNodes(), message, OverseerCollectionMessageHandler.RANDOM);
+ this.nodeList =
+ Assign.getLiveOrLiveAndCreateNodeSetList(
+ zkStateReader.getClusterState().getLiveNodes(),
+ message,
+ OverseerCollectionMessageHandler.RANDOM,
+ container.getZkController().getSolrCloudManager().getDistribStateManager());
}
@Override
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java
index cb53e234164..bd09dc37826 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java
@@ -324,7 +324,7 @@ public abstract class RoutedAlias {
SolrQueryRequest req = cmd.getReq();
SolrCore core = req.getCore();
- CoreContainer coreContainer = core.getCoreContainer();
+ CoreContainer coreContainer = req.getCoreContainer();
do {
switch (targetCollectionDesc.getCreationType()) {
case NONE:
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java
index afe1dd7923a..f8fa95c7bb0 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java
@@ -373,7 +373,7 @@ public class TimeRoutedAlias extends RoutedAlias {
} catch (DateTimeParseException e) {
startTime = DateMathParser.parseMath(new Date(), start).toInstant();
SolrCore core = cmd.getReq().getCore();
- ZkStateReader zkStateReader = core.getCoreContainer().getZkController().zkStateReader;
+ ZkStateReader zkStateReader = cmd.getReq().getCoreContainer().getZkController().zkStateReader;
Aliases aliases = zkStateReader.getAliases();
Map<String, String> props = new HashMap<>(aliases.getCollectionAliasProperties(aliasName));
start = DateTimeFormatter.ISO_INSTANT.format(startTime);
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 7d217b6dbb8..513d912fda0 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -249,6 +249,7 @@ public class CoreContainer {
protected volatile AutoscalingHistoryHandler autoscalingHistoryHandler;
private volatile SolrClientCache solrClientCache;
+ public final NodeRoles nodeRoles = new NodeRoles(System.getProperty(NodeRoles.NODE_ROLES_PROP));
private final ObjectCache objectCache = new ObjectCache();
diff --git a/solr/core/src/java/org/apache/solr/core/NodeRoles.java b/solr/core/src/java/org/apache/solr/core/NodeRoles.java
new file mode 100644
index 00000000000..abab48b1db0
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/core/NodeRoles.java
@@ -0,0 +1,152 @@
+/*
+ * 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.core;
+
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.util.StrUtils;
+
+public class NodeRoles {
+ public static final String NODE_ROLES_PROP = "solr.node.roles";
+
+ /** Roles to be assumed on nodes that don't have roles specified for them at startup */
+ public static final String DEFAULT_ROLES_STRING = "data:on,overseer:allowed";
+
+ // Map of roles to mode that are applicable for this node.
+ private Map<Role, String> nodeRoles;
+
+ public NodeRoles(String rolesString) {
+ Map<Role, String> roles = new EnumMap<>(Role.class);
+ if (StringUtils.isEmpty(rolesString)) {
+ rolesString = DEFAULT_ROLES_STRING;
+ }
+ List<String> rolesList = StrUtils.splitSmart(rolesString, ',');
+ for (String s : rolesList) {
+ List<String> roleMode = StrUtils.splitSmart(s, ':');
+ Role r = Role.getRole(roleMode.get(0));
+ String m = roleMode.get(1);
+ if (r.supportedModes().contains(m)) {
+ roles.put(r, m);
+ } else {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Unknown role mode '" + roleMode.get(1) + "' for role '" + r + "'");
+ }
+ }
+ for (Role r : Role.values()) {
+ if (!roles.containsKey(r)) {
+ roles.put(r, r.modeWhenRoleIsAbsent());
+ }
+ }
+ nodeRoles = Collections.unmodifiableMap(roles);
+ }
+
+ public Map<Role, String> getRoles() {
+ return nodeRoles;
+ }
+
+ public String getRoleMode(Role role) {
+ return nodeRoles.get(role);
+ }
+
+ public boolean isOverseerAllowedOrPreferred() {
+ String roleMode = nodeRoles.get(Role.OVERSEER);
+ return MODE_ALLOWED.equals(roleMode) || MODE_PREFERRED.equals(roleMode);
+ }
+
+ public static final String MODE_ON = "on";
+ public static final String MODE_OFF = "off";
+ public static final String MODE_ALLOWED = "allowed";
+ public static final String MODE_PREFERRED = "preferred";
+ public static final String MODE_DISALLOWED = "disallowed";
+
+ public enum Role {
+ DATA("data") {
+ @Override
+ public Set<String> supportedModes() {
+ return ImmutableSet.of(MODE_ON, MODE_OFF);
+ }
+
+ @Override
+ public String modeWhenRoleIsAbsent() {
+ return MODE_OFF;
+ }
+ },
+ OVERSEER("overseer") {
+ @Override
+ public Set<String> supportedModes() {
+ return ImmutableSet.of(MODE_ALLOWED, MODE_PREFERRED, MODE_DISALLOWED);
+ }
+
+ @Override
+ public String modeWhenRoleIsAbsent() {
+ return MODE_DISALLOWED;
+ }
+ },
+
+ COORDINATOR("coordinator") {
+ @Override
+ public String modeWhenRoleIsAbsent() {
+ return MODE_OFF;
+ }
+
+ @Override
+ public Set<String> supportedModes() {
+ return ImmutableSet.of(MODE_ON, MODE_OFF);
+ }
+ };
+
+ public final String roleName;
+
+ Role(String name) {
+ this.roleName = name;
+ }
+
+ public static Role getRole(String value) {
+ for (Role role : Role.values()) {
+ if (value.equals(role.roleName)) return role;
+ }
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown role: " + value);
+ }
+
+ public abstract Set<String> supportedModes();
+
+ /** Default mode for a role in nodes where this role is not specified. */
+ public abstract String modeWhenRoleIsAbsent();
+
+ @Override
+ public String toString() {
+ return roleName;
+ }
+ }
+
+ public static String getZNodeForRole(Role role) {
+ return ZkStateReader.NODE_ROLES + "/" + role.roleName;
+ }
+
+ public static String getZNodeForRoleMode(Role role, String mode) {
+ return ZkStateReader.NODE_ROLES + "/" + role.roleName + "/" + mode;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index a1a6d6f3ba5..02330e1ecdb 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -17,18 +17,26 @@
package org.apache.solr.handler;
+import java.io.IOException;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import com.google.common.collect.ImmutableMap;
import org.apache.solr.api.Command;
import org.apache.solr.api.EndPoint;
import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.cloud.DistribStateManager;
import org.apache.solr.client.solrj.request.beans.ClusterPropInfo;
import org.apache.solr.client.solrj.request.beans.CreateConfigInfo;
import org.apache.solr.cloud.OverseerConfigSetMessageHandler;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.cloud.ClusterProperties;
+import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ConfigSetParams;
import org.apache.solr.common.params.DefaultSolrParams;
@@ -36,10 +44,12 @@ import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.ReflectMapWriter;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.NodeRoles;
import org.apache.solr.handler.admin.CollectionsHandler;
import org.apache.solr.handler.admin.ConfigSetsHandler;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
+import org.apache.zookeeper.KeeperException;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
@@ -69,7 +79,126 @@ public class ClusterAPI {
this.collectionsHandler = ch;
this.configSetsHandler = configSetsHandler;
}
+ @EndPoint(method = GET, path = "/cluster/node-roles", permission = COLL_READ_PERM)
+ public void roles(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+ rsp.add(
+ "node-roles",
+ readRecursive(
+ ZkStateReader.NODE_ROLES,
+ collectionsHandler
+ .getCoreContainer()
+ .getZkController()
+ .getSolrCloudManager()
+ .getDistribStateManager(),
+ 3));
+ }
+
+ Object readRecursive(String path, DistribStateManager zk, int depth) {
+ if (depth == 0) return null;
+ Map<String, Object> result;
+ try {
+ List<String> children = zk.listData(path);
+ if (children != null && !children.isEmpty()) {
+ result = new HashMap<>();
+ } else {
+ return Collections.emptySet();
+ }
+ for (String child : children) {
+ Object c = readRecursive(path + "/" + child, zk, depth - 1);
+ result.put(child, c);
+ }
+ } catch (Exception e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
+ }
+ if (depth == 1) {
+ return result.keySet();
+ } else {
+ return result;
+ }
+ }
+
+ @EndPoint(method = GET, path = "/cluster/node-roles/role/{role}", permission = COLL_READ_PERM)
+ public void nodesWithRole(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+ String role = req.getPathTemplateValues().get("role");
+ rsp.add(
+ "node-roles",
+ ImmutableMap.of(
+ role,
+ readRecursive(
+ ZkStateReader.NODE_ROLES + "/" + role,
+ collectionsHandler
+ .getCoreContainer()
+ .getZkController()
+ .getSolrCloudManager()
+ .getDistribStateManager(),
+ 2)));
+ }
+
+ @EndPoint(method = GET, path = "/cluster/node-roles/node/{node}", permission = COLL_READ_PERM)
+ @SuppressWarnings("unchecked")
+ public void rolesForNode(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+ String node = req.getPathTemplateValues().get("node");
+ Map<String, String> ret = new HashMap<>();
+ Map<String, Map<String, Set<String>>> roles =
+ (Map<String, Map<String, Set<String>>>)
+ readRecursive(
+ ZkStateReader.NODE_ROLES,
+ collectionsHandler
+ .getCoreContainer()
+ .getZkController()
+ .getSolrCloudManager()
+ .getDistribStateManager(),
+ 3);
+ for (String role : roles.keySet()) {
+ for (String mode : roles.get(role).keySet()) {
+ if (roles.get(role).get(mode).isEmpty()) continue;
+ Set<String> nodes = roles.get(role).get(mode);
+ if (nodes.contains(node)) ret.put(role, mode);
+ }
+ }
+ for (String role : ret.keySet()) {
+ rsp.add(role, ret.get(role));
+ }
+ }
+
+ @EndPoint(method = GET, path = "/cluster/node-roles/supported", permission = COLL_READ_PERM)
+ public void supportedRoles(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+ Map<String, Object> roleModesSupportedMap = new HashMap<>();
+ for (NodeRoles.Role role : NodeRoles.Role.values()) {
+ roleModesSupportedMap.put(role.toString(), ImmutableMap.of("modes", role.supportedModes()));
+ }
+ rsp.add("supported-roles", roleModesSupportedMap);
+ }
+ @EndPoint(
+ method = GET,
+ path = "/cluster/node-roles/role/{role}/{mode}",
+ permission = COLL_READ_PERM)
+ public void nodesWithRoleMode(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+ // Here, deal with raw strings instead of Role & Mode types so as to handle roles and modes
+ // that are not understood by this node (possibly during a rolling upgrade)
+ String roleStr = req.getPathTemplateValues().get("role");
+ String modeStr = req.getPathTemplateValues().get("mode");
+
+ List<String> nodes =
+ collectionsHandler
+ .getCoreContainer()
+ .getZkController()
+ .getSolrCloudManager()
+ .getDistribStateManager()
+ .listData(ZkStateReader.NODE_ROLES + "/" + roleStr + "/" + modeStr);
+ rsp.add("node-roles", ImmutableMap.of(roleStr, Collections.singletonMap(modeStr, nodes)));
+ }
+
+ public static List<String> getNodesByRole(
+ NodeRoles.Role role, String mode, DistribStateManager zk)
+ throws InterruptedException, IOException, KeeperException {
+ try {
+ return zk.listData(ZkStateReader.NODE_ROLES + "/" + role + "/" + mode);
+ } catch (NoSuchElementException e) {
+ return Collections.emptyList();
+ }
+ }
@EndPoint(method = GET,
path = "/cluster/aliases",
permission = COLL_READ_PERM)
diff --git a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
index 080a696acc9..3bdcf01138c 100644
--- a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
@@ -192,7 +192,7 @@ public abstract class RequestHandlerBase implements SolrRequestHandler, SolrInfo
requests.inc();
// requests are distributed by default when ZK is in use, unless indicated otherwise
boolean distrib = req.getParams().getBool(CommonParams.DISTRIB,
- req.getCore() != null ? req.getCore().getCoreContainer().isZooKeeperAware() : false);
+ req.getCore() != null ? req.getCoreContainer().isZooKeeperAware() : false);
if (req.getParams().getBool(ShardParams.IS_SHARD, false)) {
shardPurposes.computeIfAbsent("total", name -> new Counter()).inc();
int purpose = req.getParams().getInt(ShardParams.SHARDS_PURPOSE, 0);
@@ -226,7 +226,7 @@ public abstract class RequestHandlerBase implements SolrRequestHandler, SolrInfo
}
} catch (Exception e) {
if (req.getCore() != null) {
- boolean isTragic = req.getCore().getCoreContainer().checkTragicException(req.getCore());
+ boolean isTragic = req.getCoreContainer().checkTragicException(req.getCore());
if (isTragic) {
if (e instanceof SolrException) {
// Tragic exceptions should always throw a server error
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index 03579c6321f..26bf4af349e 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -487,8 +487,8 @@ public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAwa
params.getZnodeVersion(), RequestParams.RESOURCE, params.toByteArray(), true);
log.debug("persisted to version : {} ", latestVersion);
- waitForAllReplicasState(req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(),
- req.getCore().getCoreContainer().getZkController(), RequestParams.NAME, latestVersion, 30);
+ waitForAllReplicasState(req.getCloudDescriptor().getCollectionName(),
+ req.getCoreContainer().getZkController(), RequestParams.NAME, latestVersion, 30);
}
} else {
@@ -547,13 +547,13 @@ public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAwa
int latestVersion = ZkController.persistConfigResourceToZooKeeper((ZkSolrResourceLoader) loader, overlay.getZnodeVersion(),
ConfigOverlay.RESOURCE_NAME, overlay.toByteArray(), true);
log.debug("Executed config commands successfully and persisted to ZK {}", ops);
- waitForAllReplicasState(req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(),
- req.getCore().getCoreContainer().getZkController(),
+ waitForAllReplicasState(req.getCloudDescriptor().getCollectionName(),
+ req.getCoreContainer().getZkController(),
ConfigOverlay.NAME,
latestVersion, 30);
} else {
SolrResourceLoader.persistConfLocally(loader, ConfigOverlay.RESOURCE_NAME, overlay.toByteArray());
- req.getCore().getCoreContainer().reload(req.getCore().getName(), req.getCore().uniqueId);
+ req.getCoreContainer().reload(req.getCore().getName(), req.getCore().uniqueId);
log.info("Executed config commands successfully and persisted to File System {}", ops);
}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
index 9736190bfa0..a51bd284d96 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
@@ -138,7 +138,7 @@ public class ShowFileRequestHandler extends RequestHandlerBase implements Permis
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)
throws InterruptedException, KeeperException, IOException {
- CoreContainer coreContainer = req.getCore().getCoreContainer();
+ CoreContainer coreContainer = req.getCoreContainer();
if (coreContainer.isZooKeeperAware()) {
showFromZooKeeper(req, rsp, coreContainer);
} else {
@@ -377,7 +377,7 @@ public class ShowFileRequestHandler extends RequestHandlerBase implements Permis
}
// A leading slash is unnecessary but supported and interpreted as start of config dir
Path filePath = configdir.toPath().resolve(fname.startsWith("/") ? fname.substring(1) : fname);
- req.getCore().getCoreContainer().assertPathAllowed(filePath);
+ req.getCoreContainer().assertPathAllowed(filePath);
if (!filePath.normalize().startsWith(configdir.toPath().normalize())) {
log.error("Path must be inside core config directory");
rsp.setException(
diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
index 885a272a669..3f138cd759e 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
@@ -156,8 +156,8 @@ public class HttpShardHandler extends ShardHandler {
final String shards = params.get(ShardParams.SHARDS);
CoreDescriptor coreDescriptor = req.getCore().getCoreDescriptor();
- CloudDescriptor cloudDescriptor = coreDescriptor.getCloudDescriptor();
- ZkController zkController = req.getCore().getCoreContainer().getZkController();
+ CloudDescriptor cloudDescriptor = req.getCloudDescriptor();
+ ZkController zkController = req.getCoreContainer().getZkController();
final ReplicaListTransformer replicaListTransformer = httpShardHandlerFactory.getReplicaListTransformer(req);
diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
index a2382ea9d8a..8fc980efc09 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
@@ -400,7 +400,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
final SolrParams params = req.getParams();
final SolrCore core = req.getCore(); // explicit check for null core (temporary?, for tests)
@SuppressWarnings("resource")
- ZkController zkController = core == null ? null : core.getCoreContainer().getZkController();
+ ZkController zkController = req.getCoreContainer().getZkController();
if (zkController != null) {
return requestReplicaListTransformerGenerator.getReplicaListTransformer(
params,
diff --git a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
index 934ca0aecf8..4b40f64fc7f 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
@@ -277,7 +277,7 @@ public class QueryComponent extends SearchComponent
groupingSpec.setResponseFormat(responseFormat);
// See SOLR-12249. Disallow grouping on text fields that are not SortableText in cloud mode
- if (req.getCore().getCoreContainer().isZooKeeperAware()) {
+ if (req.getCoreContainer().isZooKeeperAware()) {
IndexSchema schema = rb.req.getSchema();
String[] fields = params.getParams(GroupParams.GROUP_FIELD);
if (fields != null) {
diff --git a/solr/core/src/java/org/apache/solr/handler/component/RealTimeGetComponent.java b/solr/core/src/java/org/apache/solr/handler/component/RealTimeGetComponent.java
index faf1093a46f..bfc85ce5dac 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/RealTimeGetComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/RealTimeGetComponent.java
@@ -121,7 +121,7 @@ public class RealTimeGetComponent extends SearchComponent
SolrQueryRequest req = rb.req;
SolrQueryResponse rsp = rb.rsp;
SolrParams params = req.getParams();
- CloudDescriptor cloudDesc = req.getCore().getCoreDescriptor().getCloudDescriptor();
+ CloudDescriptor cloudDesc = req.getCloudDescriptor();
if (cloudDesc != null) {
Replica.Type replicaType = cloudDesc.getReplicaType();
@@ -173,7 +173,7 @@ public class RealTimeGetComponent extends SearchComponent
try {
if (log.isDebugEnabled()) {
log.debug("{} min count to sync to (from most recent searcher view) {}"
- , req.getCore().getCoreContainer().getZkController().getNodeName()
+ , req.getCoreContainer().getZkController().getNodeName()
, searcher.count(new MatchAllDocsQuery()));
}
} finally {
@@ -987,11 +987,11 @@ public class RealTimeGetComponent extends SearchComponent
// TODO: handle collection=...?
- ZkController zkController = rb.req.getCore().getCoreContainer().getZkController();
+ ZkController zkController = rb.req.getCoreContainer().getZkController();
// if shards=... then use that
if (zkController != null && params.get(ShardParams.SHARDS) == null) {
- CloudDescriptor cloudDescriptor = rb.req.getCore().getCoreDescriptor().getCloudDescriptor();
+ CloudDescriptor cloudDescriptor = rb.req.getCloudDescriptor();
String collection = cloudDescriptor.getCollectionName();
ClusterState clusterState = zkController.getClusterState();
@@ -1220,7 +1220,7 @@ public class RealTimeGetComponent extends SearchComponent
boolean onlyIfActive = rb.req.getParams().getBool("onlyIfActive", false);
if (onlyIfActive) {
- if (rb.req.getCore().getCoreDescriptor().getCloudDescriptor().getLastPublished() != Replica.State.ACTIVE) {
+ if (rb.req.getCloudDescriptor().getLastPublished() != Replica.State.ACTIVE) {
log.info("Last published state was not ACTIVE, cannot sync.");
rb.rsp.add("sync", "false");
return;
diff --git a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
index 660e1c75c08..7a5436f059f 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
@@ -240,8 +240,8 @@ public class SearchHandler extends RequestHandlerBase implements SolrCoreAware,
private ShardHandler getAndPrepShardHandler(SolrQueryRequest req, ResponseBuilder rb) {
ShardHandler shardHandler = null;
- CoreContainer cc = req.getCore().getCoreContainer();
- boolean isZkAware = cc.isZooKeeperAware();
+ CoreContainer cc = req.getCoreContainer();
+ boolean isZkAware = req.getCoreContainer().isZooKeeperAware();
rb.isDistrib = req.getParams().getBool(DISTRIB, isZkAware);
if (!rb.isDistrib) {
// for back compat, a shards param with URLs like localhost:8983/solr will mean that this
diff --git a/solr/core/src/java/org/apache/solr/request/DelegatingSolrQueryRequest.java b/solr/core/src/java/org/apache/solr/request/DelegatingSolrQueryRequest.java
new file mode 100644
index 00000000000..59741437a71
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/request/DelegatingSolrQueryRequest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.request;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.cloud.CloudDescriptor;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.servlet.HttpSolrCall;
+import org.apache.solr.util.RTimerTree;
+
+/**
+ * A {@link SolrQueryRequest} implementation that defers to a delegate in all cases.
+ *
+ * <p>Used primarily in cases where developers want to customize one or more SolrQueryRequest
+ * methods while deferring the remainder to an existing instances.
+ */
+public class DelegatingSolrQueryRequest implements SolrQueryRequest {
+ private final SolrQueryRequest delegate;
+
+ public DelegatingSolrQueryRequest(SolrQueryRequest delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public SolrParams getParams() {
+ return delegate.getParams();
+ }
+
+ @Override
+ public void setParams(SolrParams params) {
+ delegate.setParams(params);
+ }
+
+ @Override
+ public Iterable<ContentStream> getContentStreams() {
+ return delegate.getContentStreams();
+ }
+
+ @Override
+ public SolrParams getOriginalParams() {
+ return delegate.getOriginalParams();
+ }
+
+ @Override
+ public Map<Object, Object> getContext() {
+ return delegate.getContext();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public long getStartTime() {
+ return delegate.getStartTime();
+ }
+
+ @Override
+ public RTimerTree getRequestTimer() {
+ return delegate.getRequestTimer();
+ }
+
+ @Override
+ public SolrIndexSearcher getSearcher() {
+ return delegate.getSearcher();
+ }
+
+ @Override
+ public SolrCore getCore() {
+ return delegate.getCore();
+ }
+
+ @Override
+ public IndexSchema getSchema() {
+ return delegate.getSchema();
+ }
+
+ @Override
+ public void updateSchemaToLatest() {
+ delegate.updateSchemaToLatest();
+ }
+
+ @Override
+ public String getParamString() {
+ return delegate.getParamString();
+ }
+
+ @Override
+ public Map<String, Object> getJSON() {
+ return delegate.getJSON();
+ }
+
+ @Override
+ public void setJSON(Map<String, Object> json) {
+ delegate.setJSON(json);
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return delegate.getUserPrincipal();
+ }
+
+ @Override
+ public String getPath() {
+ return delegate.getPath();
+ }
+
+ @Override
+ public Map<String, String> getPathTemplateValues() {
+ return delegate.getPathTemplateValues();
+ }
+
+ @Override
+ public List<CommandOperation> getCommands(boolean validateInput) {
+ return delegate.getCommands(validateInput);
+ }
+
+ @Override
+ public String getHttpMethod() {
+ return delegate.getHttpMethod();
+ }
+
+ @Override
+ public HttpSolrCall getHttpSolrCall() {
+ return delegate.getHttpSolrCall();
+ }
+
+ @Override
+ public CoreContainer getCoreContainer() {
+ return delegate.getCoreContainer();
+ }
+
+ @Override
+ public CloudDescriptor getCloudDescriptor() {
+ return delegate.getCloudDescriptor();
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/request/SimpleFacets.java b/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
index 919f2e4650d..347f18eb37f 100644
--- a/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
+++ b/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
@@ -168,7 +168,7 @@ public class SimpleFacets {
this.docsOrig = docs;
this.global = params;
this.rb = rb;
- this.facetExecutor = req.getCore().getCoreContainer().getUpdateShardHandler().getUpdateExecutor();
+ this.facetExecutor = req.getCoreContainer().getUpdateShardHandler().getUpdateExecutor();
}
public void setFacetDebugInfo(FacetDebugInfo fdebugParent) {
diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
index 2c5090ba16a..17e22713893 100644
--- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
+++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
@@ -16,11 +16,15 @@
*/
package org.apache.solr.request;
+
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
+import org.apache.solr.cloud.CloudDescriptor;
+
import org.apache.solr.servlet.HttpSolrCall;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.util.RTimerTree;
@@ -132,8 +136,14 @@ public interface SolrQueryRequest extends AutoCloseable {
default HttpSolrCall getHttpSolrCall() {
return null;
}
-}
-
+ default CoreContainer getCoreContainer() {
+ SolrCore core = getCore();
+ return core == null ? null : core.getCoreContainer();
+ }
+ default CloudDescriptor getCloudDescriptor() {
+ return getCore().getCoreDescriptor().getCloudDescriptor();
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java b/solr/core/src/java/org/apache/solr/response/transform/CoreAugmenterFactory.java
similarity index 64%
copy from solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java
copy to solr/core/src/java/org/apache/solr/response/transform/CoreAugmenterFactory.java
index b40a7a25c25..eb53a99a296 100644
--- a/solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/CoreAugmenterFactory.java
@@ -14,23 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.solr.common.cloud;
-import java.util.Set;
+package org.apache.solr.response.transform;
-/**
- * Helper class to access package-private methods from ZkStateReader.
- */
-public class ZkStateReaderAccessor {
- ZkStateReader zkStateReader;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.request.SolrQueryRequest;
- public ZkStateReaderAccessor(ZkStateReader zkStateReader) {
- this.zkStateReader = zkStateReader;
- }
+public class CoreAugmenterFactory extends TransformerFactory {
- public Set<DocCollectionWatcher> getStateWatchers(String collection) {
- return zkStateReader.getStateWatchers(collection);
+ @Override
+ public DocTransformer create(String field, SolrParams params, SolrQueryRequest req) {
+ return new ValueAugmenterFactory.ValueAugmenter(field, req.getCore().getName());
}
-
-
}
diff --git a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
index ed8a9302b98..87288311608 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
@@ -51,5 +51,7 @@ public abstract class TransformerFactory implements NamedListInitializedPlugin
defaultFactories.put( "json", new RawValueTransformerFactory("json") );
defaultFactories.put( "xml", new RawValueTransformerFactory("xml") );
defaultFactories.put( "geo", new GeoTransformerFactory() );
+ defaultFactories.put("core", new CoreAugmenterFactory());
+
}
}
diff --git a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
index 33845141964..3aa11d5a356 100644
--- a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
@@ -167,7 +167,7 @@ public class JoinQParserPlugin extends QParserPlugin {
long fromCoreOpenTime = 0;
if (fromIndex != null && !fromIndex.equals(qparser.req.getCore().getCoreDescriptor().getName()) ) {
- CoreContainer container = qparser.req.getCore().getCoreContainer();
+ CoreContainer container = qparser.req.getCoreContainer();
// if in SolrCloud mode, fromIndex should be the name of a single-sharded collection
coreName = ScoreJoinQParserPlugin.getCoreName(fromIndex, container);
diff --git a/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
index 51f2dcc1f81..7cd7607d4a5 100644
--- a/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
@@ -90,7 +90,7 @@ public class ScoreJoinQParserPlugin extends QParserPlugin {
public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
- CoreContainer container = info.getReq().getCore().getCoreContainer();
+ CoreContainer container = info.getReq().getCoreContainer();
final SolrCore fromCore = container.getCore(fromIndex);
@@ -228,7 +228,7 @@ public class ScoreJoinQParserPlugin extends QParserPlugin {
final String myCore = req.getCore().getCoreDescriptor().getName();
if (fromIndex != null && (!fromIndex.equals(myCore) || byPassShortCircutCheck)) {
- CoreContainer container = req.getCore().getCoreContainer();
+ CoreContainer container = req.getCoreContainer();
final String coreName = getCoreName(fromIndex, container);
final SolrCore fromCore = container.getCore(coreName);
diff --git a/solr/core/src/java/org/apache/solr/servlet/CoordinatorHttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/CoordinatorHttpSolrCall.java
new file mode 100644
index 00000000000..9cc1a89d922
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/servlet/CoordinatorHttpSolrCall.java
@@ -0,0 +1,352 @@
+/*
+ * 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.servlet;
+
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.solr.api.CoordinatorV2HttpSolrCall;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.cloud.CloudDescriptor;
+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.ZkStateReader;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.CoreDescriptor;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.logging.MDCLoggingContext;
+import org.apache.solr.request.DelegatingSolrQueryRequest;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A coordinator node can serve requests as if it hosts all collections in the cluster. it does so
+ * by hosting a synthetic replica for each configset used in the cluster.
+ *
+ * <p>This class is responsible for forwarding the requests to the right core when the node is
+ * acting as a Coordinator The responsibilities also involve creating a synthetic collection or
+ * replica if they do not exist. It also sets the right threadlocal variables which reflects the
+ * current collection being served.
+ */
+public class CoordinatorHttpSolrCall extends HttpSolrCall {
+ public static final String SYNTHETIC_COLL_PREFIX =
+ ".sys." + "COORDINATOR-COLL-";
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private String collectionName;
+ private final Factory factory;
+
+ public CoordinatorHttpSolrCall(
+ Factory factory,
+ SolrDispatchFilter solrDispatchFilter,
+ CoreContainer cores,
+ HttpServletRequest request,
+ HttpServletResponse response,
+ boolean retry) {
+ super(solrDispatchFilter, cores, request, response, retry);
+ this.factory = factory;
+ }
+
+ @Override
+ protected SolrCore getCoreByCollection(String collectionName, boolean isPreferLeader) {
+ log.info("getCoreByCollection(String collectionName({})", collectionName);
+ this.collectionName = collectionName;
+ SolrCore core = super.getCoreByCollection(collectionName, isPreferLeader);
+ if (core != null) return core;
+ if (!path.endsWith("/select")) return null;
+ return getCore(factory, this, collectionName, isPreferLeader);
+ }
+
+ public static SolrCore getCore(
+ Factory factory, HttpSolrCall solrCall, String collectionName, boolean isPreferLeader) {
+ log.info("CoordinatorHttpSolrCall#getCore({})", collectionName);
+ String syntheticCoreName = factory.collectionVsCoreNameMapping.get(collectionName);
+ if (syntheticCoreName != null) {
+ SolrCore syntheticCore = solrCall.cores.getCore(syntheticCoreName);
+ setMDCLoggingContext(collectionName);
+ return syntheticCore;
+ } else {
+ ZkStateReader zkStateReader = solrCall.cores.getZkController().getZkStateReader();
+ ClusterState clusterState = zkStateReader.getClusterState();
+ DocCollection coll = clusterState.getCollectionOrNull(collectionName, true);
+ SolrCore core = null;
+ if (coll != null) {
+ String confName = null;
+ try {
+ confName = zkStateReader.readConfigName(collectionName);
+ } catch (KeeperException e) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Could not locate config for collection ["
+ + collectionName
+ + "] ", e);
+ }
+ String syntheticCollectionName = getSyntheticCollectionName(confName);
+
+ DocCollection syntheticColl = clusterState.getCollectionOrNull(syntheticCollectionName);
+ synchronized (CoordinatorHttpSolrCall.class) {
+ if (syntheticColl == null) {
+ // no synthetic collection for this config, let's create one
+ if (log.isInfoEnabled()) {
+ log.info(
+ "synthetic collection: {} does not exist, creating.. ", syntheticCollectionName);
+ }
+
+ SolrException createException = null;
+ try {
+ createColl(syntheticCollectionName, solrCall.cores, confName);
+ } catch (SolrException exception) {
+ // concurrent requests could have created the collection hence causing collection
+ // exists
+ // exception
+ createException = exception;
+ } finally {
+ syntheticColl =
+ zkStateReader.getClusterState().getCollectionOrNull(syntheticCollectionName);
+ }
+
+ // then indeed the collection was not created properly, either by this or other
+ // concurrent
+ // requests
+ if (syntheticColl == null) {
+ if (createException != null) {
+ throw createException; // rethrow the exception since such collection was not
+ // created
+ } else {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Could not locate synthetic collection ["
+ + syntheticCollectionName
+ + "] after creation!");
+ }
+ }
+ }
+
+ // get docCollection again to ensure we get the fresh state
+ syntheticColl =
+ zkStateReader.getClusterState().getCollectionOrNull(syntheticCollectionName);
+ List<Replica> nodeNameSyntheticReplicas =
+ syntheticColl.getReplicas(solrCall.cores.getZkController().getNodeName());
+ if (nodeNameSyntheticReplicas == null || nodeNameSyntheticReplicas.isEmpty()) {
+ // this node does not have a replica. add one
+ if (log.isInfoEnabled()) {
+ log.info(
+ "this node does not have a replica of the synthetic collection: {} , adding replica ",
+ syntheticCollectionName);
+ }
+
+ addReplica(syntheticCollectionName, solrCall.cores);
+ }
+
+ // still have to ensure that it's active, otherwise super.getCoreByCollection
+ // will return null and then CoordinatorHttpSolrCall will call getCore again
+ // hence creating a calling loop
+ try {
+ zkStateReader.waitForState(
+ syntheticCollectionName,
+ 10,
+ TimeUnit.SECONDS,
+ docCollection -> {
+ for (Replica nodeNameSyntheticReplica :
+ docCollection.getReplicas(solrCall.cores.getZkController().getNodeName())) {
+ if (nodeNameSyntheticReplica.getState() == Replica.State.ACTIVE) {
+ return true;
+ }
+ }
+ return false;
+ });
+ } catch (Exception e) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Failed to wait for active replica for synthetic collection ["
+ + syntheticCollectionName
+ + "]",
+ e);
+ }
+ }
+
+ core = solrCall.getCoreByCollection(syntheticCollectionName, isPreferLeader);
+ if (core != null) {
+ factory.collectionVsCoreNameMapping.put(collectionName, core.getName());
+ // for the watcher, only remove on collection deletion (ie collection == null), since
+ // watch from coordinator is collection specific
+ solrCall
+ .cores
+ .getZkController()
+ .getZkStateReader()
+ .registerDocCollectionWatcher(
+ collectionName,
+ collection -> {
+ if (collection == null) {
+ factory.collectionVsCoreNameMapping.remove(collectionName);
+ return true;
+ } else {
+ return false;
+ }
+ });
+ if (log.isDebugEnabled()) {
+ log.debug("coordinator node, returns synthetic core: {}", core.getName());
+ }
+ }
+ setMDCLoggingContext(collectionName);
+ return core;
+ }
+ return null;
+ }
+ }
+
+ public static String getSyntheticCollectionName(String configName) {
+ return SYNTHETIC_COLL_PREFIX + configName;
+ }
+
+ /**
+ * Overrides the MDC context as the core set was synthetic core, which does not reflect the
+ * collection being operated on
+ */
+ private static void setMDCLoggingContext(String collectionName) {
+ MDCLoggingContext.setCollection(collectionName);
+
+ // below is irrelevant for call to coordinator
+ MDCLoggingContext.setCoreName(null);
+ MDCLoggingContext.setShard(null);
+ MDCLoggingContext.setCoreName(null);
+ }
+
+ private static void addReplica(String syntheticCollectionName, CoreContainer cores) {
+ SolrQueryResponse rsp = new SolrQueryResponse();
+ try {
+ CollectionAdminRequest.AddReplica addReplicaRequest =
+ CollectionAdminRequest.addReplicaToShard(syntheticCollectionName, "shard1")
+ // we are fixing the name, so that no two replicas are created in the same node
+ .setNode(cores.getZkController().getNodeName());
+ addReplicaRequest.setWaitForFinalState(true);
+ cores
+ .getCollectionsHandler()
+ .handleRequestBody(new LocalSolrQueryRequest(null, addReplicaRequest.getParams()), rsp);
+ if (rsp.getValues().get("success") == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Could not auto-create collection: " + Utils.toJSONString(rsp.getValues()));
+ }
+ } catch (Exception e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
+ }
+ }
+
+ private static void createColl(
+ String syntheticCollectionName, CoreContainer cores, String confName) {
+ SolrQueryResponse rsp = new SolrQueryResponse();
+ try {
+ CollectionAdminRequest.Create collCreationRequest =
+ CollectionAdminRequest.createCollection(syntheticCollectionName, confName, 1, 1)
+ .setCreateNodeSet(cores.getZkController().getNodeName());
+ collCreationRequest.setWaitForFinalState(true);
+ SolrParams params = collCreationRequest.getParams();
+ if (log.isInfoEnabled()) {
+ log.info("sending collection admin command : {}", Utils.toJSONString(params));
+ }
+ cores.getCollectionsHandler().handleRequestBody(new LocalSolrQueryRequest(null, params), rsp);
+ if (rsp.getValues().get("success") == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Could not create :"
+ + syntheticCollectionName
+ + " collection: "
+ + Utils.toJSONString(rsp.getValues()));
+ }
+ } catch (SolrException e) {
+ throw e;
+
+ } catch (Exception e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
+ }
+ }
+
+ @Override
+ protected void init() throws Exception {
+ super.init();
+ if (action == SolrDispatchFilter.Action.PROCESS && core != null) {
+ solrReq = wrappedReq(solrReq, collectionName, this);
+ }
+ }
+
+ @Override
+ protected String getCoreOrColName() {
+ return collectionName;
+ }
+
+ public static SolrQueryRequest wrappedReq(
+ SolrQueryRequest delegate, String collectionName, HttpSolrCall httpSolrCall) {
+ if(collectionName== null) return delegate;
+ Properties p = new Properties();
+ log.info("CoordinatorHttpSolrCall#wrappedReq({})",collectionName);
+ p.put(CoreDescriptor.CORE_COLLECTION, collectionName);
+ p.put(CloudDescriptor.REPLICA_TYPE, Replica.Type.PULL.toString());
+ p.put(CoreDescriptor.CORE_SHARD, "_");
+
+ CloudDescriptor cloudDescriptor =
+ new CloudDescriptor(
+ delegate.getCore().getCoreDescriptor(), delegate.getCore().getName(), p);
+ return new DelegatingSolrQueryRequest(delegate) {
+ @Override
+ public HttpSolrCall getHttpSolrCall() {
+ return httpSolrCall;
+ }
+
+ @Override
+ public CloudDescriptor getCloudDescriptor() {
+ return cloudDescriptor;
+ }
+ };
+ }
+
+ // The factory that creates an instance of HttpSolrCall
+ public static class Factory implements SolrDispatchFilter.HttpSolrCallFactory {
+ private final Map<String, String> collectionVsCoreNameMapping = new ConcurrentHashMap<>();
+
+ @Override
+ public HttpSolrCall createInstance(
+ SolrDispatchFilter filter,
+ String path,
+ CoreContainer cores,
+ HttpServletRequest request,
+ HttpServletResponse response,
+ boolean retry) {
+ if ((path.startsWith("/____v2/") || path.equals("/____v2"))) {
+ return new CoordinatorV2HttpSolrCall(this, filter, cores, request, response, retry);
+ } else if (path.startsWith("/" + SYNTHETIC_COLL_PREFIX)) {
+ return SolrDispatchFilter.HttpSolrCallFactory.super.createInstance(
+ filter, path, cores, request, response, retry);
+ } else {
+ return new CoordinatorHttpSolrCall(this, filter, cores, request, response, retry);
+ }
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
index c58d52b9f44..607e7d1f617 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -228,6 +228,8 @@ public class HttpSolrCall {
}
queryParams = SolrRequestParsers.parseQueryString(req.getQueryString());
+ log.info("HttpSolrCall.init({}?{})", req.getRequestURL(), req.getQueryString());
+
// unused feature ?
int idx = path.indexOf(':');
@@ -603,6 +605,13 @@ public class HttpSolrCall {
}
}
+ protected String getCoreOrColName() {
+ String coreOrColName = HttpSolrCall.this.origCorename;
+ if (coreOrColName == null && getCore() != null) {
+ coreOrColName = getCore().getName();
+ }
+ return coreOrColName;
+ }
private boolean shouldAudit() {
return cores.getAuditLoggerPlugin() != null;
diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
index 9f0a7b3f275..90b977d6c56 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
@@ -69,6 +69,7 @@ import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.NodeConfig;
+import org.apache.solr.core.NodeRoles;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrInfoBean;
import org.apache.solr.core.SolrXmlConfig;
@@ -103,6 +104,8 @@ public class SolrDispatchFilter extends BaseSolrFilter {
protected final CountDownLatch init = new CountDownLatch(1);
protected String abortErrorMessage = null;
+ private HttpSolrCallFactory solrCallFactory;
+
//TODO using Http2Client
protected HttpClient httpClient;
private ArrayList<Pattern> excludePatterns;
@@ -182,6 +185,13 @@ public class SolrDispatchFilter extends BaseSolrFilter {
}
coresInit = createCoreContainer(computeSolrHome(config), extraProperties);
+ boolean isCoordinator =
+ NodeRoles.MODE_ON.equals(
+ coresInit
+ .nodeRoles
+ .getRoleMode(NodeRoles.Role.COORDINATOR));
+ solrCallFactory =
+ isCoordinator ? new CoordinatorHttpSolrCall.Factory() : new HttpSolrCallFactory() {};
this.httpClient = coresInit.getUpdateShardHandler().getDefaultHttpClient();
setupJvmMetrics(coresInit);
@@ -479,11 +489,13 @@ public class SolrDispatchFilter extends BaseSolrFilter {
protected HttpSolrCall getHttpSolrCall(HttpServletRequest request, HttpServletResponse response, boolean retry) {
String path = ServletUtils.getPathAfterContext(request);
- if (isV2Enabled && (path.startsWith("/____v2/") || path.equals("/____v2"))) {
- return new V2HttpCall(this, cores, request, response, false);
- } else {
- return new HttpSolrCall(this, cores, request, response, retry);
+ CoreContainer cores = null;
+ try {
+ cores = getCores();
+ } catch (Exception e) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container Unavailable");
}
+ return solrCallFactory.createInstance(this, path, cores, request, response, retry);
}
private boolean authenticateRequest(HttpServletRequest request, HttpServletResponse response, final AtomicReference<HttpServletRequest> wrappedRequest) throws IOException {
@@ -681,4 +693,19 @@ public class SolrDispatchFilter extends BaseSolrFilter {
public void closeOnDestroy(boolean closeOnDestroy) {
this.closeOnDestroy = closeOnDestroy;
}
+ public interface HttpSolrCallFactory {
+ default HttpSolrCall createInstance(
+ SolrDispatchFilter filter,
+ String path,
+ CoreContainer cores,
+ HttpServletRequest request,
+ HttpServletResponse response,
+ boolean retry) {
+ if (filter.isV2Enabled && (path.startsWith("/____v2/") || path.equals("/____v2"))) {
+ return new V2HttpCall(filter, cores, request, response, retry);
+ } else {
+ return new HttpSolrCall(filter, cores, request, response, retry);
+ }
+ }
+ }
}
diff --git a/solr/core/src/java/org/apache/solr/update/UpdateLog.java b/solr/core/src/java/org/apache/solr/update/UpdateLog.java
index 789f1455a50..590ce372e8e 100644
--- a/solr/core/src/java/org/apache/solr/update/UpdateLog.java
+++ b/solr/core/src/java/org/apache/solr/update/UpdateLog.java
@@ -1838,7 +1838,7 @@ public class UpdateLog implements PluginInfoInitialized, SolrMetricProducer {
UpdateRequestProcessorChain processorChain = req.getCore().getUpdateProcessingChain(null);
UpdateRequestProcessor proc = processorChain.createProcessor(req, rsp);
- OrderedExecutor executor = inSortedOrder ? null : req.getCore().getCoreContainer().getReplayUpdatesExecutor();
+ OrderedExecutor executor = inSortedOrder ? null : req.getCoreContainer().getReplayUpdatesExecutor();
AtomicInteger pendingTasks = new AtomicInteger(0);
AtomicReference<SolrException> exceptionOnExecuteUpdate = new AtomicReference<>();
diff --git a/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessorFactory.java b/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessorFactory.java
index 93c1bf2871e..deaa1d9f9f5 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessorFactory.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/DistributedUpdateProcessorFactory.java
@@ -50,7 +50,7 @@ public class DistributedUpdateProcessorFactory
public UpdateRequestProcessor getInstance(SolrQueryRequest req,
SolrQueryResponse rsp, UpdateRequestProcessor next) {
- final boolean isZkAware = req.getCore().getCoreContainer().isZooKeeperAware();
+ final boolean isZkAware = req.getCoreContainer().isZooKeeperAware();
DistributedUpdateProcessor distribUpdateProcessor =
isZkAware ?
diff --git a/solr/core/src/java/org/apache/solr/update/processor/RoutedAliasUpdateProcessor.java b/solr/core/src/java/org/apache/solr/update/processor/RoutedAliasUpdateProcessor.java
index d95f946578b..112eae2a883 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/RoutedAliasUpdateProcessor.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/RoutedAliasUpdateProcessor.java
@@ -93,10 +93,10 @@ public class RoutedAliasUpdateProcessor extends UpdateRequestProcessor {
// todo: a core should have a more direct way of finding a collection name, and the collection properties
SolrCore core = req.getCore();
CoreDescriptor coreDescriptor = core.getCoreDescriptor();
- CloudDescriptor cloudDescriptor = coreDescriptor.getCloudDescriptor();
+ CloudDescriptor cloudDescriptor = req.getCloudDescriptor();
if (cloudDescriptor != null) {
String collectionName = cloudDescriptor.getCollectionName();
- CoreContainer coreContainer = core.getCoreContainer();
+ CoreContainer coreContainer = req.getCoreContainer();
ZkController zkController = coreContainer.getZkController();
ZkStateReader zkStateReader = zkController.getZkStateReader();
Map<String, String> collectionProperties = zkStateReader.getCollectionProperties(collectionName, CACHE_FOR_MILLIS);
@@ -127,7 +127,7 @@ public class RoutedAliasUpdateProcessor extends UpdateRequestProcessor {
}
private static Map<String, String> getAliasProps(SolrQueryRequest req, String aliasName) {
- ZkController zkController = req.getCore().getCoreContainer().getZkController();
+ ZkController zkController = req.getCoreContainer().getZkController();
final Map<String, String> aliasProperties = zkController.getZkStateReader().getAliases().getCollectionAliasProperties(aliasName);
if (aliasProperties.isEmpty()) {
throw RoutedAlias.newAliasMustExistException(aliasName); // if it did exist, we'd have a non-null map
@@ -141,8 +141,8 @@ public class RoutedAliasUpdateProcessor extends UpdateRequestProcessor {
this.routedAlias = routedAlias;
assert aliasDistribPhase == DistribPhase.NONE;
final SolrCore core = req.getCore();
- final CoreContainer cc = core.getCoreContainer();
- this.thisCollection = core.getCoreDescriptor().getCloudDescriptor().getCollectionName();
+ final CoreContainer cc = req.getCoreContainer();
+ this.thisCollection = req.getCloudDescriptor().getCollectionName();
this.req = req;
this.zkController = cc.getZkController();
this.cmdDistrib = new SolrCmdDistributor(cc.getUpdateShardHandler());
diff --git a/solr/core/src/test-files/solr/configsets/cache-control/conf/schema.xml b/solr/core/src/test-files/solr/configsets/cache-control/conf/schema.xml
new file mode 100644
index 00000000000..cef2b0a12e3
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/cache-control/conf/schema.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<schema name="minimal" version="1.1">
+ <fieldType name="string" class="solr.StrField"/>
+ <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+ <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+ <dynamicField name="*" type="string" indexed="true" stored="true"/>
+ <!-- for versioning -->
+ <field name="_version_" type="long" indexed="true" stored="true"/>
+ <field name="_root_" type="string" indexed="true" stored="true" multiValued="false" required="false"/>
+ <field name="id" type="string" indexed="true" stored="true"/>
+ <dynamicField name="*_s" type="string" indexed="true" stored="true" />
+ <uniqueKey>id</uniqueKey>
+</schema>
\ No newline at end of file
diff --git a/solr/core/src/test-files/solr/configsets/cache-control/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/cache-control/conf/solrconfig.xml
new file mode 100644
index 00000000000..ee9a217c06a
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/cache-control/conf/solrconfig.xml
@@ -0,0 +1,54 @@
+<?xml version="1.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.
+-->
+
+<!-- Minimal solrconfig.xml with /select, /admin and /update only -->
+
+<config>
+
+ <dataDir>${solr.data.dir:}</dataDir>
+
+ <directoryFactory name="DirectoryFactory"
+ class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+ <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+ <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+ <updateHandler class="solr.DirectUpdateHandler2">
+ <commitWithin>
+ <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+ </commitWithin>
+ <updateLog class="${solr.ulog:solr.UpdateLog}"></updateLog>
+ </updateHandler>
+
+ <requestDispatcher>
+ <httpCaching>
+ <cacheControl>max-age=30, public</cacheControl>
+ </httpCaching>
+ </requestDispatcher>
+
+ <requestHandler name="/select" class="solr.SearchHandler">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <str name="indent">true</str>
+ <str name="df">text</str>
+ </lst>
+
+ </requestHandler>
+ <indexConfig>
+ <mergeScheduler class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/>
+ : </indexConfig>
+</config>
\ No newline at end of file
diff --git a/solr/core/src/test-files/solr/configsets/conf3/conf/schema.xml b/solr/core/src/test-files/solr/configsets/conf3/conf/schema.xml
new file mode 100644
index 00000000000..85f7ed35453
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/conf3/conf/schema.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<schema name="minimal" version="1.1">
+ <fieldType name="string" class="solr.StrField"/>
+ <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+ <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+ <dynamicField name="*" type="string" indexed="true" stored="true"/>
+ <!-- for versioning -->
+ <field name="_version_" type="long" indexed="true" stored="true"/>
+ <field name="_root_" type="string" indexed="true" stored="true" multiValued="false" required="false"/>
+ <field name="id" type="string" indexed="true" stored="true"/>
+ <dynamicField name="*_s" type="string" indexed="true" stored="true" />
+ <uniqueKey>id</uniqueKey>
+ <query>
+ <filterCache
+ size="0"
+ initialSize="0"
+ autowarmCount="0"/>
+ <queryResultCache
+ size="0"
+ initialSize="0"
+ autowarmCount="0"/>
+ <documentCache
+ size="0"
+ initialSize="0"
+ autowarmCount="0"/>
+ </query>
+</schema>
diff --git a/solr/core/src/test-files/solr/configsets/conf3/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/conf3/conf/solrconfig.xml
new file mode 100644
index 00000000000..0b3ec4d24ce
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/conf3/conf/solrconfig.xml
@@ -0,0 +1,68 @@
+<?xml version="1.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.
+-->
+
+<!-- Minimal solrconfig.xml with /select, /admin and /update only -->
+
+<config>
+
+ <dataDir>${solr.data.dir:}</dataDir>
+
+ <directoryFactory name="DirectoryFactory"
+ class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+ <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+ <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+ <updateHandler class="solr.DirectUpdateHandler2">
+ <commitWithin>
+ <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+ </commitWithin>
+ <updateLog class="${solr.ulog:solr.UpdateLog}"></updateLog>
+ </updateHandler>
+
+ <requestHandler name="/select" class="solr.SearchHandler">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <str name="indent">true</str>
+ <str name="df">text</str>
+ </lst>
+
+ </requestHandler>
+ <query>
+ <filterCache
+ class = "org.apache.solr.search.CaffeineCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0" />
+
+ <queryResultCache
+ size="512"
+ initialSize="512"
+ autowarmCount="0" />
+
+ <documentCache
+ size="512"
+ initialSize="512"
+ autowarmCount="0" />
+ </query>
+ <indexConfig>
+ <mergeScheduler class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/>
+ </indexConfig>
+</config>
+
diff --git a/solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java b/solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java
index b40a7a25c25..2f682fb10c0 100644
--- a/solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java
+++ b/solr/core/src/test/org/apache/solr/common/cloud/ZkStateReaderAccessor.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.common.cloud;
+import java.util.Collections;
import java.util.Set;
/**
@@ -32,5 +33,7 @@ public class ZkStateReaderAccessor {
return zkStateReader.getStateWatchers(collection);
}
-
+ public Set<String> getWatchedCollections() {
+ return Collections.unmodifiableSet(zkStateReader.getCollectionWatches().keySet());
+ }
}
diff --git a/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java b/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java
new file mode 100644
index 00000000000..430efe573e1
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java
@@ -0,0 +1,889 @@
+/*
+ * 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.search;
+
+import static org.apache.solr.common.params.CommonParams.OMIT_HEADER;
+import static org.apache.solr.common.params.CommonParams.TRUE;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+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.cloud.ZkStateReader;
+import org.apache.solr.common.cloud.ZkStateReaderAccessor;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.core.NodeRoles;
+import org.apache.solr.servlet.CoordinatorHttpSolrCall;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestCoordinatorRole extends SolrCloudTestCase {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public void testSimple() throws Exception {
+ MiniSolrCloudCluster cluster =
+ configureCluster(4).addConfig("conf", configset("cloud-minimal")).configure();
+ try {
+ CloudSolrClient client = cluster.getSolrClient();
+ String COLLECTION_NAME = "test_coll";
+ String SYNTHETIC_COLLECTION = CoordinatorHttpSolrCall.getSyntheticCollectionName("conf");
+ CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf", 2, 2)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(COLLECTION_NAME, 2, 4);
+ UpdateRequest ur = new UpdateRequest();
+ for (int i = 0; i < 10; i++) {
+ SolrInputDocument doc2 = new SolrInputDocument();
+ doc2.addField("id", "" + i);
+ ur.add(doc2);
+ }
+
+ ur.commit(client, COLLECTION_NAME);
+ QueryResponse rsp = client.query(COLLECTION_NAME, new SolrQuery("*:*"));
+ assertEquals(10, rsp.getResults().getNumFound());
+
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ final JettySolrRunner coordinatorJetty;
+ try {
+ coordinatorJetty = cluster.startJettySolrRunner();
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+ QueryResponse rslt =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, COLLECTION_NAME);
+
+ assertEquals(10, rslt.getResults().size());
+
+ DocCollection collection =
+ cluster.getSolrClient().getClusterStateProvider().getCollection(SYNTHETIC_COLLECTION);
+ assertNotNull(collection);
+
+ Set<String> expectedNodes = new HashSet<>();
+ expectedNodes.add(coordinatorJetty.getNodeName());
+ collection.forEachReplica((s, replica) -> expectedNodes.remove(replica.getNodeName()));
+ assertTrue(expectedNodes.isEmpty());
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public void testMultiCollectionMultiNode() throws Exception {
+ MiniSolrCloudCluster cluster =
+ configureCluster(4).addConfig("conf", configset("cloud-minimal")).configure();
+ try {
+ CloudSolrClient client = cluster.getSolrClient();
+ String COLLECTION_NAME = "test_coll";
+ String SYNTHETIC_COLLECTION = CoordinatorHttpSolrCall.getSyntheticCollectionName("conf");
+ for (int j = 1; j <= 10; j++) {
+ String collname = COLLECTION_NAME + "_" + j;
+ CollectionAdminRequest.createCollection(collname, "conf", 2, 2)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collname, 2, 4);
+ UpdateRequest ur = new UpdateRequest();
+ for (int i = 0; i < 10; i++) {
+ SolrInputDocument doc2 = new SolrInputDocument();
+ doc2.addField("id", "" + i);
+ ur.add(doc2);
+ }
+
+ ur.commit(client, collname);
+ QueryResponse rsp = client.query(collname, new SolrQuery("*:*"));
+ assertEquals(10, rsp.getResults().getNumFound());
+ }
+
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ final JettySolrRunner coordinatorJetty1;
+ final JettySolrRunner coordinatorJetty2;
+ try {
+ coordinatorJetty1 = cluster.startJettySolrRunner();
+ coordinatorJetty2 = cluster.startJettySolrRunner();
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+ for (int j = 1; j <= 10; j++) {
+ String collname = COLLECTION_NAME + "_" + j;
+ QueryResponse rslt =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty1.getNodeName()))
+ .process(client, collname);
+
+ assertEquals(10, rslt.getResults().size());
+ }
+
+ for (int j = 1; j <= 10; j++) {
+ String collname = COLLECTION_NAME + "_" + j;
+ QueryResponse rslt =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty2.getNodeName()))
+ .process(client, collname);
+
+ assertEquals(10, rslt.getResults().size());
+ }
+
+ DocCollection collection =
+ cluster.getSolrClient().getClusterStateProvider().getCollection(SYNTHETIC_COLLECTION);
+ assertNotNull(collection);
+
+ int coordNode1NumCores = coordinatorJetty1.getCoreContainer().getNumAllCores();
+ assertEquals("Unexpected number of cores found for coordinator node", 1, coordNode1NumCores);
+ int coordNode2NumCores = coordinatorJetty2.getCoreContainer().getNumAllCores();
+ assertEquals("Unexpected number of cores found for coordinator node", 1, coordNode2NumCores);
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public void testNRTRestart() throws Exception {
+ // we restart jetty and expect to find on disk data - need a local fs directory
+ useFactory(null);
+ String COLL = "coordinator_test_coll";
+ MiniSolrCloudCluster cluster =
+ configureCluster(3)
+ .withJettyConfig(jetty -> jetty.enableV2(true))
+ .addConfig("conf", configset("conf3"))
+ .configure();
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ JettySolrRunner qaJetty = cluster.startJettySolrRunner();
+ String qaJettyBase = qaJetty.getBaseUrl().toString();
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ ExecutorService executor =
+ ExecutorUtil.newMDCAwareSingleThreadExecutor(new SolrNamedThreadFactory("manipulateJetty"));
+ try {
+ CollectionAdminRequest.createCollection(COLL, "conf", 1, 1, 0, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(COLL, 1, 2);
+ DocCollection docColl =
+ cluster.getSolrClient().getClusterStateProvider().getClusterState().getCollection(COLL);
+ Replica nrtReplica = docColl.getReplicas(EnumSet.of(Replica.Type.NRT)).get(0);
+ assertNotNull(nrtReplica);
+ String nrtCore = nrtReplica.getCoreName();
+ Replica pullReplica = docColl.getReplicas(EnumSet.of(Replica.Type.PULL)).get(0);
+ assertNotNull(pullReplica);
+ String pullCore = pullReplica.getCoreName();
+
+ SolrInputDocument sid = new SolrInputDocument();
+ sid.addField("id", "123");
+ sid.addField("desc_s", "A Document");
+ JettySolrRunner nrtJetty = null;
+ JettySolrRunner pullJetty = null;
+ for (JettySolrRunner j : cluster.getJettySolrRunners()) {
+ String nodeName = j.getNodeName();
+ if (nodeName.equals(nrtReplica.getNodeName())) {
+ nrtJetty = j;
+ } else if (nodeName.equals(pullReplica.getNodeName())) {
+ pullJetty = j;
+ }
+ }
+ assertNotNull(nrtJetty);
+ assertNotNull(pullJetty);
+ try (SolrClient client = pullJetty.newClient()) {
+ client.add(COLL, sid);
+ client.commit(COLL);
+ assertEquals(
+ nrtCore,
+ getHostCoreName(
+ COLL, qaJettyBase, p -> p.add(ShardParams.SHARDS_PREFERENCE, "replica.type:NRT")));
+ assertEquals(
+ pullCore,
+ getHostCoreName(
+ COLL, qaJettyBase, p -> p.add(ShardParams.SHARDS_PREFERENCE, "replica.type:PULL")));
+ // Now , kill NRT jetty
+ JettySolrRunner nrtJettyF = nrtJetty;
+ JettySolrRunner pullJettyF = pullJetty;
+ Random r = random();
+ final long establishBaselineMs = r.nextInt(1000);
+ final long nrtDowntimeMs = r.nextInt(10000);
+ // NOTE: for `pullServiceTimeMs`, it can't be super-short. This is just to simplify our
+ // indexing code,
+ // based on the fact that our indexing is based on a PULL-node client.
+ final long pullServiceTimeMs = 1000 + (long) r.nextInt(9000);
+ Future<?> jettyManipulationFuture =
+ executor.submit(
+ () -> {
+ // we manipulate the jetty instances in a separate thread to more closely mimic
+ // the behavior we'd see irl.
+ try {
+ Thread.sleep(establishBaselineMs);
+ log.info("stopping NRT jetty ...");
+ nrtJettyF.stop();
+ log.info("NRT jetty stopped.");
+ Thread.sleep(nrtDowntimeMs); // let NRT be down for a while
+ log.info("restarting NRT jetty ...");
+ nrtJettyF.start(true);
+ log.info("NRT jetty restarted.");
+ // once NRT is back up, we expect PULL to continue serving until the TTL on ZK
+ // state used for query request routing has expired (60s). But here we force a
+ // return to NRT by stopping the PULL replica after a brief delay ...
+ Thread.sleep(pullServiceTimeMs);
+ log.info("stopping PULL jetty ...");
+ pullJettyF.stop();
+ log.info("PULL jetty stopped.");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ String hostCore;
+ long start = new Date().getTime();
+ long individualRequestStart = start;
+ int count = 0;
+ while (nrtCore.equals(
+ hostCore =
+ getHostCoreName(
+ COLL,
+ qaJettyBase,
+ p -> p.add(ShardParams.SHARDS_PREFERENCE, "replica.type:NRT")))) {
+ count++;
+ Thread.sleep(100);
+ individualRequestStart = new Date().getTime();
+ }
+ long now = new Date().getTime();
+ log.info(
+ "phase1 NRT queries count={}, overall_duration={}, baseline_expected_overall_duration={}, switch-to-pull_duration={}",
+ count,
+ now - start,
+ establishBaselineMs,
+ now - individualRequestStart);
+ // default tolerance of 500ms below should suffice. Failover to PULL for this case should be
+ // very fast, because our QA-based client already knows both replicas are active, the index
+ // is stable, so the moment the client finds NRT is down it should be able to failover
+ // immediately and transparently to PULL.
+ assertEquals(
+ "when we break out of the NRT query loop, should be b/c routed to PULL",
+ pullCore,
+ hostCore);
+ SolrInputDocument d = new SolrInputDocument();
+ d.addField("id", "345");
+ d.addField("desc_s", "Another Document");
+ // attempts to add another doc while NRT is down should fail, then eventually succeed when
+ // NRT comes back up
+ count = 0;
+ start = new Date().getTime();
+ individualRequestStart = start;
+ for (; ; ) {
+ try {
+ client.add(COLL, d);
+ client.commit(COLL);
+ break;
+ } catch (SolrException ex) {
+ // we expect these until nrtJetty is back up.
+ count++;
+ Thread.sleep(100);
+ }
+ individualRequestStart = new Date().getTime();
+ }
+ now = new Date().getTime();
+ log.info(
+ "successfully added another doc; duration: {}, overall_duration={}, baseline_expected_overall_duration={}, exception_count={}",
+ now - individualRequestStart,
+ now - start,
+ nrtDowntimeMs,
+ count);
+ // NRT replica is back up, registered as available with Zk, and availability info has been
+ // pulled down by our PULL-replica-based `client`, forwarded indexing command to NRT,
+ // index/commit completed. All of this accounts for the 3000ms tolerance allowed for below.
+ // This is not a strict value, and if it causes failures regularly we should feel free to
+ // increase the tolerance; but it's meant to provide a stable baseline from which to detect
+ // regressions.
+ count = 0;
+ start = new Date().getTime();
+ individualRequestStart = start;
+ while (pullCore.equals(
+ hostCore =
+ getHostCoreName(
+ COLL,
+ qaJettyBase,
+ p -> {
+ p.set(CommonParams.Q, "id:345");
+ p.add(ShardParams.SHARDS_PREFERENCE, "replica.type:NRT");
+ }))) {
+ count++;
+ Thread.sleep(100);
+ individualRequestStart = new Date().getTime();
+ }
+ now = new Date().getTime();
+ log.info(
+ "query retries between NRT index-ready and query-ready: {}; overall_duration={}; baseline_expected_overall_duration={}; failover-request_duration={}",
+ count,
+ now - start,
+ pullServiceTimeMs,
+ now - individualRequestStart);
+ assertEquals(nrtCore, hostCore);
+ // allow any exceptions to propagate
+ jettyManipulationFuture.get();
+
+ // next phase: just toggle a bunch
+ // TODO: could separate this out into a different test method, but this should suffice for
+ // now
+ pullJetty.start(true);
+ AtomicBoolean done = new AtomicBoolean();
+ long runMinutes = 1;
+ long finishTimeMs =
+ new Date().getTime() + TimeUnit.MILLISECONDS.convert(runMinutes, TimeUnit.MINUTES);
+ JettySolrRunner[] jettys = new JettySolrRunner[] {nrtJettyF, pullJettyF};
+ Random threadRandom = new Random(r.nextInt());
+ Future<Integer> f =
+ executor.submit(
+ () -> {
+ int iteration = 0;
+ while (new Date().getTime() < finishTimeMs && !done.get()) {
+ int idx = iteration++ % jettys.length;
+ JettySolrRunner toManipulate = jettys[idx];
+ try {
+ int serveTogetherTime = threadRandom.nextInt(7000);
+ int downTime = threadRandom.nextInt(7000);
+ log.info("serving together for {}ms", serveTogetherTime);
+ Thread.sleep(serveTogetherTime);
+ log.info("stopping {} ...", idx);
+ toManipulate.stop();
+ log.info("stopped {}.", idx);
+ Thread.sleep(downTime);
+ log.info("restarting {} ...", idx);
+ toManipulate.start(true);
+ log.info("restarted {}.", idx);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ done.set(true);
+ return iteration;
+ });
+ count = 0;
+ start = new Date().getTime();
+ try {
+ do {
+ if (pullCore.equals(
+ getHostCoreName(
+ COLL,
+ qaJettyBase,
+ p -> {
+ p.set(CommonParams.Q, "id:345");
+ p.add(ShardParams.SHARDS_PREFERENCE, "replica.type:NRT");
+ }))) {
+ done.set(true);
+ }
+ count++;
+ Thread.sleep(100);
+ } while (!done.get());
+ } finally {
+ final String result;
+ if (done.getAndSet(true)) {
+ result = "Success";
+ } else {
+ // not yet set to done, completed abnormally (exception will be thrown beyond `finally`
+ // block)
+ result = "Failure";
+ }
+ Integer toggleCount = f.get();
+ long secondsDuration =
+ TimeUnit.SECONDS.convert(new Date().getTime() - start, TimeUnit.MILLISECONDS);
+ log.info(
+ "{}! {} seconds, {} toggles, {} requests served",
+ result,
+ secondsDuration,
+ toggleCount,
+ count);
+ }
+ }
+ } finally {
+ try {
+ ExecutorUtil.shutdownAndAwaitTermination(executor);
+ } finally {
+ cluster.shutdown();
+ }
+ }
+ }
+
+ private String getHostCoreName(String COLL, String qaNode, Consumer<SolrQuery> p)
+ throws Exception {
+ boolean found = false;
+ SolrQuery q =
+ new SolrQuery(
+ CommonParams.Q,
+ "*:*",
+ CommonParams.FL,
+ "id,desc_s,_core_:[core]",
+ OMIT_HEADER,
+ TRUE,
+ CommonParams.WT,
+ CommonParams.JAVABIN);
+ p.accept(q);
+ SolrDocumentList docs = null;
+ try (SolrClient solrClient = new Http2SolrClient.Builder(qaNode).build()) {
+ for (int i = 0; i < 100; i++) {
+ try {
+ QueryResponse queryResponse = solrClient.query(COLL, q);
+ docs = queryResponse.getResults();
+ assertNotNull("Docs should not be null. Query response was: " + queryResponse, docs);
+ if (docs.size() > 0) {
+ found = true;
+ break;
+ }
+ } catch (SolrException ex) {
+ // we know we're doing tricky things that might cause transient errors
+ // TODO: all these query requests go to the QA node -- should QA propagate internal
+ // request errors to the external client (and the external client retry?) or should QA
+ // attempt to failover transparently in the event of an error?
+ if (i < 5) {
+ log.info("swallowing transient error", ex);
+ } else {
+ log.error("only expect actual _errors_ within a small window (e.g. 500ms)", ex);
+ fail("initial error time threshold exceeded");
+ }
+ }
+ Thread.sleep(100);
+ }
+ }
+ assertTrue(found);
+ return (String) docs.get(0).getFieldValue("_core_");
+ }
+
+ public void testConcurrentAccess() throws Exception {
+ final int DATA_NODE_COUNT = 2;
+ final int COORDINATOR_NODE_COUNT = 4;
+ MiniSolrCloudCluster cluster =
+ configureCluster(DATA_NODE_COUNT).addConfig("conf", configset("cloud-minimal")).configure();
+
+ List<String> dataNodes =
+ cluster.getJettySolrRunners().stream()
+ .map(JettySolrRunner::getNodeName)
+ .collect(Collectors.toList());
+
+ try {
+ CloudSolrClient client = cluster.getSolrClient();
+ String COLLECTION_PREFIX = "test_coll_";
+
+ final int COLLECTION_COUNT = 10;
+ final int DOC_PER_COLLECTION_COUNT = 1000;
+
+ List<String> collectionNames = new ArrayList<>();
+ for (int i = 0; i < COLLECTION_COUNT; i++) {
+ String collectionName = COLLECTION_PREFIX + i;
+ CollectionAdminRequest.createCollection(collectionName, "conf", 2, 1)
+ .setCreateNodeSet(String.join(",", dataNodes)) // only put data onto the 2 data nodes
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collectionName, 2, 2);
+ collectionNames.add(collectionName);
+ }
+
+ for (String collectionName : collectionNames) {
+ UpdateRequest ur = new UpdateRequest();
+ for (int i = 0; i < DOC_PER_COLLECTION_COUNT; i++) {
+ SolrInputDocument doc2 = new SolrInputDocument();
+ doc2.addField("id", collectionName + "-" + i);
+ ur.add(doc2);
+ }
+ ur.commit(client, collectionName);
+ QueryResponse rsp = client.query(collectionName, new SolrQuery("*:*"));
+ assertEquals(DOC_PER_COLLECTION_COUNT, rsp.getResults().getNumFound());
+ }
+
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ List<String> coordinatorNodes = new ArrayList<>();
+ try {
+ for (int i = 0; i < COORDINATOR_NODE_COUNT; i++) {
+ JettySolrRunner coordinatorJetty = cluster.startJettySolrRunner();
+ coordinatorNodes.add(coordinatorJetty.getNodeName());
+ }
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+
+ int THREAD_COUNT = 10;
+ int RUN_COUNT = 20;
+ // final AtomicInteger runCounter = new AtomicInteger();
+ // 10 threads to concurrently access the collections and ensure data are not mixed up
+ ExecutorService executorService =
+ ExecutorUtil.newMDCAwareFixedThreadPool(
+ THREAD_COUNT, new SolrNamedThreadFactory(this.getClass().getSimpleName()));
+ List<Future<?>> testFutures = new ArrayList<>();
+
+ for (int i = 0; i < RUN_COUNT; i++) {
+ final int currentRun = i;
+ testFutures.add(
+ executorService.submit(
+ () -> {
+ final String collectionName =
+ collectionNames.get(currentRun % collectionNames.size());
+ final String coordinatorNode =
+ coordinatorNodes.get(currentRun % coordinatorNodes.size());
+ QueryResponse response =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorNode))
+ .process(client, collectionName);
+ assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+ // ensure docs have the correct id (ie not mixing up with other collections)
+ for (SolrDocument doc : response.getResults()) {
+ assertTrue(((String) doc.getFieldValue("id")).startsWith(collectionName));
+ }
+ return null;
+ }));
+ }
+ for (Future<?> testFuture : testFutures) {
+ testFuture.get(); // check for any exceptions/failures
+ }
+
+ // number of replicas created in the synthetic collection should be one per coordinator node
+ assertEquals(
+ COORDINATOR_NODE_COUNT,
+ client
+ .getZkStateReader()
+ .getClusterState()
+ .getCollection(CoordinatorHttpSolrCall.getSyntheticCollectionName("conf"))
+ .getReplicas()
+ .size());
+
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public void testConfigset() throws Exception {
+ final int DATA_NODE_COUNT = 1;
+ MiniSolrCloudCluster cluster =
+ configureCluster(DATA_NODE_COUNT)
+ .addConfig("conf1", configset("cloud-minimal"))
+ .addConfig("conf2", configset("cache-control"))
+ .configure();
+ List<String> dataNodes =
+ cluster.getJettySolrRunners().stream()
+ .map(JettySolrRunner::getNodeName)
+ .collect(Collectors.toList());
+
+ try {
+ CollectionAdminRequest.createCollection("c1", "conf1", 2, 1)
+ .setMaxShardsPerNode(10)
+ . process(cluster.getSolrClient());
+ cluster.waitForActiveCollection("c1", 2, 2);
+ CollectionAdminRequest.createCollection("c2", "conf2", 2, 1)
+ .setMaxShardsPerNode(10)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection("c2", 2, 2);
+
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ JettySolrRunner coordinatorJetty;
+ try {
+ coordinatorJetty = cluster.startJettySolrRunner();
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+
+ // Tricky to test configset, since operation such as collection status would direct it to the
+ // OS node.
+ // So we use query and check the cache response header, which is determined by the
+ // solr-config.xml in the configset
+ // However using solr client would drop cache response header, hence we need to use the
+ // underlying httpClient which has SSL correctly configured
+
+ try (HttpSolrClient solrClient =
+ new HttpSolrClient.Builder(coordinatorJetty.getBaseUrl().toString()).build()) {
+ HttpResponse response =
+ solrClient
+ .getHttpClient()
+ .execute(new HttpGet(coordinatorJetty.getBaseUrl() + "/c1/select?q=*:*"));
+ // conf1 has no cache-control
+ assertNull(response.getFirstHeader("cache-control"));
+
+ response =
+ solrClient
+ .getHttpClient()
+ .execute(new HttpGet(coordinatorJetty.getBaseUrl() + "/c2/select?q=*:*"));
+ // conf2 has cache-control defined
+ assertTrue(response.getFirstHeader("cache-control").getValue().contains("max-age=30"));
+ }
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public void testWatch() throws Exception {
+ final int DATA_NODE_COUNT = 1;
+ MiniSolrCloudCluster cluster =
+ configureCluster(DATA_NODE_COUNT)
+ .addConfig("conf1", configset("cloud-minimal"))
+ .configure();
+ final String TEST_COLLECTION_1 = "c1";
+ final String TEST_COLLECTION_2 = "c2";
+
+ try {
+ CloudSolrClient client = cluster.getSolrClient();
+ CollectionAdminRequest.createCollection(TEST_COLLECTION_1, "conf1", 1, 2)
+ .setMaxShardsPerNode(10)
+ .process(client);
+ cluster.waitForActiveCollection(TEST_COLLECTION_1, 1, 2);
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ JettySolrRunner coordinatorJetty;
+ try {
+ coordinatorJetty = cluster.startJettySolrRunner();
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+
+ ZkStateReader zkStateReader =
+ coordinatorJetty.getCoreContainer().getZkController().getZkStateReader();
+ ZkStateReaderAccessor zkWatchAccessor = new ZkStateReaderAccessor(zkStateReader);
+
+ // no watch at first
+ assertTrue(!zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+ new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, TEST_COLLECTION_1); // ok no exception thrown
+
+ // now it should be watching it after the query
+ assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+
+ // add another collection
+ CollectionAdminRequest.createCollection(TEST_COLLECTION_2, "conf1", 1, 2).process(client);
+ cluster.waitForActiveCollection(TEST_COLLECTION_2, 1, 2);
+ new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, TEST_COLLECTION_2);
+ // watch both collections
+ assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+ assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_2));
+
+ CollectionAdminRequest.deleteReplica(TEST_COLLECTION_1, "shard1", 1).process(client);
+ cluster.waitForActiveCollection(TEST_COLLECTION_1, 1, 1);
+ new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, TEST_COLLECTION_1); // ok no exception thrown
+
+ // still one replica left, should not remove the watch
+ assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+
+ // now delete c1 and ensure it's cleared from various logic
+ CollectionAdminRequest.deleteCollection(TEST_COLLECTION_1).process(client);
+ zkStateReader.waitForState(TEST_COLLECTION_1, 30, TimeUnit.SECONDS, Objects::isNull);
+ assertNull(zkStateReader.getCollection(TEST_COLLECTION_1)); // check the cluster state
+
+ // ensure querying throws exception
+ assertExceptionThrownWithMessageContaining(
+ SolrException.class,
+ ImmutableList.of("Collection not found"),
+ () ->
+ new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, TEST_COLLECTION_1));
+
+ // watch should be removed after c1 deletion
+ assertTrue(!zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+ // still watching c2
+ assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_2));
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public void testSplitShard() throws Exception {
+ final int DATA_NODE_COUNT = 1;
+ MiniSolrCloudCluster cluster =
+ configureCluster(DATA_NODE_COUNT)
+ .addConfig("conf1", configset("cloud-minimal"))
+ .configure();
+
+ try {
+
+ final String COLLECTION_NAME = "c1";
+ CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf1", 1, 1)
+ .setMaxShardsPerNode(10)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(COLLECTION_NAME, 1, 1);
+
+ int DOC_PER_COLLECTION_COUNT = 1000;
+ UpdateRequest ur = new UpdateRequest();
+ for (int i = 0; i < DOC_PER_COLLECTION_COUNT; i++) {
+ SolrInputDocument doc = new SolrInputDocument();
+ doc.addField("id", COLLECTION_NAME + "-" + i);
+ ur.add(doc);
+ }
+ CloudSolrClient client = cluster.getSolrClient();
+ ur.commit(client, COLLECTION_NAME);
+
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ JettySolrRunner coordinatorJetty;
+ try {
+ coordinatorJetty = cluster.startJettySolrRunner();
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+
+ QueryResponse response = (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, COLLECTION_NAME);
+
+ assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+
+ // now split the shard
+ CollectionAdminRequest.splitShard(COLLECTION_NAME).setShardName("shard1").process(client);
+ waitForState(
+ "Failed to wait for child shards after split",
+ COLLECTION_NAME,
+ (liveNodes, collectionState) ->
+ collectionState.getSlice("shard1_0") != null
+ && collectionState.getSlice("shard1_0").getState() == Slice.State.ACTIVE
+ && collectionState.getSlice("shard1_1") != null
+ && collectionState.getSlice("shard1_1").getState() == Slice.State.ACTIVE);
+
+ // delete the parent shard
+ CollectionAdminRequest.deleteShard(COLLECTION_NAME, "shard1").process(client);
+ waitForState(
+ "Parent shard is not yet deleted after split",
+ COLLECTION_NAME,
+ (liveNodes, collectionState) -> collectionState.getSlice("shard1") == null);
+
+ response =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, COLLECTION_NAME);
+
+ assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public void testMoveReplica() throws Exception {
+ final int DATA_NODE_COUNT = 2;
+ MiniSolrCloudCluster cluster =
+ configureCluster(DATA_NODE_COUNT)
+ .addConfig("conf1", configset("cloud-minimal"))
+ .configure();
+
+ List<String> dataNodes =
+ cluster.getJettySolrRunners().stream()
+ .map(JettySolrRunner::getNodeName)
+ .collect(Collectors.toList());
+ try {
+
+ final String COLLECTION_NAME = "c1";
+ String fromNode = dataNodes.get(0); // put the shard on first data node
+ CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf1", 1, 1)
+ .setCreateNodeSet(fromNode)
+ .process(cluster.getSolrClient());
+ // ensure replica is placed on the expected node
+ waitForState(
+ "Cannot find replica on first node yet",
+ COLLECTION_NAME,
+ (liveNodes, collectionState) -> {
+ if (collectionState.getReplicas().size() == 1) {
+ Replica replica = collectionState.getReplicas().get(0);
+ return fromNode.equals(replica.getNodeName())
+ && replica.getState() == Replica.State.ACTIVE;
+ }
+ return false;
+ });
+
+ int DOC_PER_COLLECTION_COUNT = 1000;
+ UpdateRequest ur = new UpdateRequest();
+ for (int i = 0; i < DOC_PER_COLLECTION_COUNT; i++) {
+ SolrInputDocument doc = new SolrInputDocument();
+ doc.addField("id", COLLECTION_NAME + "-" + i);
+ ur.add(doc);
+ }
+ CloudSolrClient client = cluster.getSolrClient();
+ ur.commit(client, COLLECTION_NAME);
+
+ System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+ JettySolrRunner coordinatorJetty;
+ try {
+ coordinatorJetty = cluster.startJettySolrRunner();
+ } finally {
+ System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+ }
+
+ QueryResponse response =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, COLLECTION_NAME);
+
+ assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+
+ // now move the shard/replica
+ String replicaName = getCollectionState(COLLECTION_NAME).getReplicas().get(0).getName();
+ String toNodeName = dataNodes.get(1);
+ CollectionAdminRequest.moveReplica(COLLECTION_NAME, replicaName, toNodeName).process(client);
+ waitForState(
+ "Cannot find replica on second node yet after repliac move",
+ COLLECTION_NAME,
+ (liveNodes, collectionState) -> {
+ if (collectionState.getReplicas().size() == 1) {
+ Replica replica = collectionState.getReplicas().get(0);
+ return toNodeName.equals(replica.getNodeName())
+ && replica.getState() == Replica.State.ACTIVE;
+ }
+ return false;
+ });
+
+ // We must stop the first node to ensure that query directs to the correct node from
+ // coordinator.
+ // In case if coordinator node has the wrong info (replica on first node), it might still
+ // return valid result if
+ // we do not stop the first node as first node might forward the query to second node.
+ cluster.getJettySolrRunners().get(0).stop();
+
+ response =
+ (QueryResponse) new QueryRequest(new SolrQuery("*:*"))
+ .setPreferredNodes(ImmutableList.of(coordinatorJetty.getNodeName()))
+ .process(client, COLLECTION_NAME);
+
+ assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+ } finally {
+ cluster.shutdown();
+ }
+ }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
index 51e92dbbaa8..edfd3d26af1 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -66,7 +67,8 @@ public abstract class SolrRequest<T extends SolrResponse> implements Serializabl
private METHOD method = METHOD.GET;
private String path = null;
- private Map<String,String> headers;
+ private Map<String, String> headers;
+ private List<String> preferredNodes;
private ResponseParser responseParser;
private StreamingResponseCallback callback;
@@ -92,6 +94,15 @@ public abstract class SolrRequest<T extends SolrResponse> implements Serializabl
return this;
}
+ public SolrRequest<T> setPreferredNodes(List<String> nodes) {
+ this.preferredNodes = nodes;
+ return this;
+ }
+
+ public List<String> getPreferredNodes() {
+ return this.preferredNodes;
+ }
+
private String basicAuthUser, basicAuthPwd;
private String basePath;
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseCloudSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseCloudSolrClient.java
index a224a6098a1..9b6de9a9835 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseCloudSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseCloudSolrClient.java
@@ -1112,6 +1112,21 @@ public abstract class BaseCloudSolrClient extends SolrClient {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"No collection param specified on request and no default collection has been set: " + inputCollections);
}
+ List<String> preferredNodes = request.getPreferredNodes();
+ if (preferredNodes != null && !preferredNodes.isEmpty()) {
+ String joinedInputCollections = StrUtils.join(inputCollections, ',');
+ List<String> urlList = new ArrayList<>(preferredNodes.size());
+ for (String nodeName : preferredNodes) {
+ urlList.add(
+ Utils.getBaseUrlForNodeName(nodeName, urlScheme) + "/" + joinedInputCollections);
+ }
+ if (!urlList.isEmpty()) {
+ LBSolrClient.Req req = new LBSolrClient.Req(request, urlList);
+ LBSolrClient.Rsp rsp = getLbClient().request(req);
+ return rsp.getResponse();
+ }
+ }
+
// TODO: not a big deal because of the caching, but we could avoid looking
// at every shard when getting leaders if we tweaked some things
diff --git a/solr/solrj/src/java/org/apache/solr/common/cloud/ZkStateReader.java b/solr/solrj/src/java/org/apache/solr/common/cloud/ZkStateReader.java
index 10d3a925b67..adb705ab324 100644
--- a/solr/solrj/src/java/org/apache/solr/common/cloud/ZkStateReader.java
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/ZkStateReader.java
@@ -110,6 +110,12 @@ public class ZkStateReader implements SolrCloseable {
public static final String STATE_TIMESTAMP_PROP = "stateTimestamp";
public static final String COLLECTIONS_ZKNODE = "/collections";
public static final String LIVE_NODES_ZKNODE = "/live_nodes";
+ /**
+ * The following, node_roles and roles.json are for assigning roles to nodes. The node_roles is
+ * the preferred way (using -Dsolr.node.roles param), and roles.json is used by legacy ADDROLE API
+ * command.
+ */
+ public static final String NODE_ROLES = "/node_roles";
public static final String ALIASES = "/aliases.json";
public static final String CLUSTER_STATE = "/clusterstate.json";
public static final String CLUSTER_PROPS = "/clusterprops.json";
@@ -2077,6 +2083,10 @@ public class ZkStateReader implements SolrCloseable {
return updated;
}
+ /* package-private for testing*/
+ Map<String, CollectionWatch<DocCollectionWatcher>> getCollectionWatches() {
+ return Collections.unmodifiableMap(collectionWatches);
+ }
public void registerCollectionPropsWatcher(final String collection, CollectionPropsWatcher propsWatcher) {
AtomicBoolean watchSet = new AtomicBoolean(false);
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBadInputTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBadInputTest.java
index 1c9196c13af..679930e3cf2 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBadInputTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBadInputTest.java
@@ -21,14 +21,11 @@ import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
-import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.junit.BeforeClass;
import org.junit.Test;
-import static org.hamcrest.core.StringContains.containsString;
-
public class CloudHttp2SolrClientBadInputTest extends SolrCloudTestCase {
private static final List<String> NULL_STR_LIST = null;
private static final List<String> EMPTY_STR_LIST = new ArrayList<>();
@@ -60,16 +57,4 @@ public class CloudHttp2SolrClientBadInputTest extends SolrCloudTestCase {
});
}
}
-
- private void assertExceptionThrownWithMessageContaining(@SuppressWarnings({"rawtypes"})Class expectedType,
- List<String> expectedStrings, LuceneTestCase.ThrowingRunnable runnable) {
- @SuppressWarnings({"unchecked"})
- Throwable thrown = expectThrows(expectedType, runnable);
-
- if (expectedStrings != null) {
- for (String expectedString : expectedStrings) {
- assertThat(thrown.getMessage(), containsString(expectedString));
- }
- }
- }
}
\ No newline at end of file
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientBadInputTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientBadInputTest.java
index acace0dc732..9a104d406b4 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientBadInputTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientBadInputTest.java
@@ -21,14 +21,11 @@ import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
-import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.junit.BeforeClass;
import org.junit.Test;
-import static org.hamcrest.core.StringContains.containsString;
-
public class CloudSolrClientBadInputTest extends SolrCloudTestCase {
private static final List<String> NULL_STR_LIST = null;
private static final List<String> EMPTY_STR_LIST = new ArrayList<>();
@@ -60,16 +57,4 @@ public class CloudSolrClientBadInputTest extends SolrCloudTestCase {
});
}
}
-
- private void assertExceptionThrownWithMessageContaining(@SuppressWarnings({"rawtypes"})Class expectedType,
- List<String> expectedStrings, LuceneTestCase.ThrowingRunnable runnable) {
- @SuppressWarnings({"unchecked"})
- Throwable thrown = expectThrows(expectedType, runnable);
-
- if (expectedStrings != null) {
- for (String expectedString : expectedStrings) {
- assertThat(thrown.getMessage(), containsString(expectedString));
- }
- }
- }
}
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateHttp2SolrClientBadInputTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateHttp2SolrClientBadInputTest.java
index 6c2728b3e92..dcd7058b23f 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateHttp2SolrClientBadInputTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateHttp2SolrClientBadInputTest.java
@@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
-import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.SolrJettyTestBase;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.JettyConfig;
@@ -85,17 +84,4 @@ public class ConcurrentUpdateHttp2SolrClientBadInputTest extends SolrJettyTestBa
});
}
}
-
- private void assertExceptionThrownWithMessageContaining(@SuppressWarnings({"rawtypes"})Class expectedType,
- List<String> expectedStrings, LuceneTestCase.ThrowingRunnable runnable) {
- @SuppressWarnings({"unchecked"})
- Throwable thrown = expectThrows(expectedType, runnable);
-
- if (expectedStrings != null) {
- for (String expectedString : expectedStrings) {
- assertTrue("[" + thrown.getMessage() + "] doesn't contain [" + expectedString + "]",
- thrown.getMessage().contains(expectedString));
- }
- }
- }
}
\ No newline at end of file
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientBadInputTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientBadInputTest.java
index 9db0e4d0f64..d911a15a1bc 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientBadInputTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientBadInputTest.java
@@ -21,15 +21,12 @@ import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
-import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.SolrJettyTestBase;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.JettyConfig;
import org.junit.BeforeClass;
import org.junit.Test;
-import static org.hamcrest.core.StringContains.containsString;
-
public class ConcurrentUpdateSolrClientBadInputTest extends SolrJettyTestBase {
private static final List<String> NULL_STR_LIST = null;
private static final List<String> EMPTY_STR_LIST = new ArrayList<>();
@@ -78,16 +75,4 @@ public class ConcurrentUpdateSolrClientBadInputTest extends SolrJettyTestBase {
});
}
}
-
- private void assertExceptionThrownWithMessageContaining(@SuppressWarnings({"rawtypes"})Class expectedType,
- List<String> expectedStrings, LuceneTestCase.ThrowingRunnable runnable) {
- @SuppressWarnings({"unchecked"})
- Throwable thrown = expectThrows(expectedType, runnable);
-
- if (expectedStrings != null) {
- for (String expectedString : expectedStrings) {
- assertThat(thrown.getMessage(), containsString(expectedString));
- }
- }
- }
}
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientBadInputTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientBadInputTest.java
index 4b1eccc0d4a..10d583db2a5 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientBadInputTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientBadInputTest.java
@@ -26,7 +26,6 @@ import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.JettyConfig;
import org.junit.BeforeClass;
import org.junit.Test;
-import static org.hamcrest.core.StringContains.containsString;
/**
* Tests {@link HttpSolrClient}'s response to a variety of bad inputs.
@@ -45,18 +44,6 @@ public class HttpSolrClientBadInputTest extends SolrJettyTestBase {
createAndStartJetty(legacyExampleCollection1SolrHome(), jettyConfig);
}
- private void assertExceptionThrownWithMessageContaining(@SuppressWarnings({"rawtypes"})Class expectedType,
- List<String> expectedStrings, ThrowingRunnable runnable) {
- @SuppressWarnings({"unchecked"})
- Throwable thrown = expectThrows(expectedType, runnable);
-
- if (expectedStrings != null) {
- for (String expectedString : expectedStrings) {
- assertThat(thrown.getMessage(), containsString(expectedString));
- }
- }
- }
-
@Test
public void testDeleteByIdReportsInvalidIdLists() throws Exception {
try (SolrClient client = getHttpSolrClient(jetty.getBaseUrl().toString() + "/" + ANY_COLLECTION)) {
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttpSolrClientBadInputTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttpSolrClientBadInputTest.java
index 6ce46f0c896..5085e6f4733 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttpSolrClientBadInputTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttpSolrClientBadInputTest.java
@@ -21,15 +21,12 @@ import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
-import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.SolrJettyTestBase;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.JettyConfig;
import org.junit.BeforeClass;
import org.junit.Test;
-import static org.hamcrest.core.StringContains.containsString;
-
public class LBHttpSolrClientBadInputTest extends SolrJettyTestBase {
private static final List<String> NULL_STR_LIST = null;
private static final List<String> EMPTY_STR_LIST = new ArrayList<>();
@@ -76,16 +73,4 @@ public class LBHttpSolrClientBadInputTest extends SolrJettyTestBase {
});
}
}
-
- private void assertExceptionThrownWithMessageContaining(@SuppressWarnings({"rawtypes"})Class expectedType,
- List<String> expectedStrings, LuceneTestCase.ThrowingRunnable runnable) {
- @SuppressWarnings({"unchecked"})
- Throwable thrown = expectThrows(expectedType, runnable);
-
- if (expectedStrings != null) {
- for (String expectedString : expectedStrings) {
- assertThat(thrown.getMessage(), containsString(expectedString));
- }
- }
- }
}
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 e229061628f..6e4723561c8 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
@@ -146,6 +146,7 @@ import org.apache.solr.util.StartupLoggingUtils;
import org.apache.solr.util.TestHarness;
import org.apache.solr.util.TestInjection;
import org.apache.zookeeper.KeeperException;
+import org.hamcrest.MatcherAssert;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -168,6 +169,7 @@ import static org.apache.solr.common.cloud.ZkStateReader.HTTPS;
import static org.apache.solr.common.cloud.ZkStateReader.URL_SCHEME;
import static org.apache.solr.update.processor.DistributedUpdateProcessor.DistribPhase;
import static org.apache.solr.update.processor.DistributingUpdateProcessorFactory.DISTRIB_UPDATE_PARAM;
+import static org.hamcrest.core.StringContains.containsString;
/**
* A junit4 Solr test harness that extends SolrTestCase and, by extension, LuceneTestCase.
@@ -225,6 +227,18 @@ public abstract class SolrTestCaseJ4 extends SolrTestCase {
properties.store(writer, testname);
}
}
+ protected void assertExceptionThrownWithMessageContaining(
+ Class<? extends Throwable> expectedType,
+ List<String> expectedStrings,
+ ThrowingRunnable runnable) {
+ Throwable thrown = expectThrows(expectedType, runnable);
+
+ if (expectedStrings != null) {
+ for (String expectedString : expectedStrings) {
+ MatcherAssert.assertThat(thrown.getMessage(), containsString(expectedString));
+ }
+ }
+ }
/**
* Annotation for test classes that want to disable SSL