You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by kr...@apache.org on 2016/11/28 17:26:44 UTC
[15/42] lucene-solr:jira/solr-8593: SOLR-9784: Refactor
CloudSolrClient to eliminate direct dependency on ZK SOLR-9512:
CloudSolrClient's cluster state cache can break direct updates to leaders
SOLR-9784: Refactor CloudSolrClient to eliminate direct dependency on ZK
SOLR-9512: CloudSolrClient's cluster state cache can break direct updates to leaders
Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/d87ffa4b
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/d87ffa4b
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/d87ffa4b
Branch: refs/heads/jira/solr-8593
Commit: d87ffa4bf82c30e9a6f0bbb6b8c0087a5c07f9d6
Parents: 41d7b24
Author: Noble Paul <no...@apache.org>
Authored: Fri Nov 25 00:27:16 2016 +0530
Committer: Noble Paul <no...@apache.org>
Committed: Fri Nov 25 00:51:09 2016 +0530
----------------------------------------------------------------------
solr/CHANGES.txt | 6 +-
solr/solrj/ivy.xml | 4 +
.../impl/ZkClientClusterStateProvider.java | 177 ++++++++++++++++
.../apache/solr/common/cloud/ClusterState.java | 7 +
.../apache/solr/common/cloud/ZkStateReader.java | 10 +-
.../solrj/impl/CloudSolrClientCacheTest.java | 206 +++++++++++++++++++
6 files changed, 406 insertions(+), 4 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/d87ffa4b/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 4d9e6a0..fe67490 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -196,7 +196,9 @@ Bug Fixes
* SOLR-9729: JDBCStream improvements (Kevin Risden)
* SOLR-9626: new Admin UI now also highlights matched terms in the Analysis screen. (Alexandre Rafalovitch)
-
+
+* SOLR-9512: CloudSolrClient's cluster state cache can break direct updates to leaders (noble)
+
Other Changes
----------------------
@@ -222,6 +224,8 @@ Other Changes
* SOLR-8785: Use Dropwizard Metrics library for core metrics. The copied over code in
org.apache.solr.util.stats has been removed. (Jeff Wartes, Kelvin Wong, Christine Poerschke, shalin)
+* SOLR-9784: Refactor CloudSolrClient to eliminate direct dependency on ZK (noble)
+
================== 6.3.0 ==================
Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release.
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/d87ffa4b/solr/solrj/ivy.xml
----------------------------------------------------------------------
diff --git a/solr/solrj/ivy.xml b/solr/solrj/ivy.xml
index c9995f3..ae7d02b 100644
--- a/solr/solrj/ivy.xml
+++ b/solr/solrj/ivy.xml
@@ -40,6 +40,10 @@
<dependency org="org.slf4j" name="jcl-over-slf4j" rev="${/org.slf4j/jcl-over-slf4j}" conf="compile"/>
<dependency org="org.slf4j" name="slf4j-log4j12" rev="${/org.slf4j/slf4j-log4j12}" conf="test"/>
+ <dependency org="org.easymock" name="easymock" rev="${/org.easymock/easymock}" conf="test"/>
+ <dependency org="cglib" name="cglib-nodep" rev="${/cglib/cglib-nodep}" conf="test"/>
+ <dependency org="org.objenesis" name="objenesis" rev="${/org.objenesis/objenesis}" conf="test"/>
+
<exclude org="*" ext="*" matcher="regexp" type="${ivy.exclude.types}"/>
</dependencies>
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/d87ffa4b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ZkClientClusterStateProvider.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ZkClientClusterStateProvider.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ZkClientClusterStateProvider.java
new file mode 100644
index 0000000..5541186
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ZkClientClusterStateProvider.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.client.solrj.impl;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.Aliases;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.cloud.ZooKeeperException;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+class ZkClientClusterStateProvider implements CloudSolrClient.ClusterStateProvider {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+
+ ZkStateReader zkStateReader;
+ String zkHost;
+ int zkConnectTimeout = 10000;
+ int zkClientTimeout = 10000;
+
+ public ZkClientClusterStateProvider(Collection<String> zkHosts, String chroot) {
+ zkHost = buildZkHostString(zkHosts,chroot);
+ }
+
+ public ZkClientClusterStateProvider(String zkHost){
+ this.zkHost = zkHost;
+ }
+
+ @Override
+ public ClusterState.CollectionRef getState(String collection) {
+ return zkStateReader.getClusterState().getCollectionRef(collection);
+ }
+
+ @Override
+ public Set<String> liveNodes() {
+ return zkStateReader.getClusterState().getLiveNodes();
+ }
+
+
+ @Override
+ public String getAlias(String collection) {
+ Aliases aliases = zkStateReader.getAliases();
+ return aliases.getCollectionAlias(collection);
+ }
+
+ @Override
+ public Map<String, Object> getClusterProperties() {
+ return zkStateReader.getClusterProperties();
+ }
+
+ @Override
+ public String getCollectionName(String name) {
+ Aliases aliases = zkStateReader.getAliases();
+ if (aliases != null) {
+ Map<String, String> collectionAliases = aliases.getCollectionAliasMap();
+ if (collectionAliases != null && collectionAliases.containsKey(name)) {
+ name = collectionAliases.get(name);
+ }
+ }
+ return name;
+ }
+ /**
+ * Download a named config from Zookeeper to a location on the filesystem
+ * @param configName the name of the config
+ * @param downloadPath the path to write config files to
+ * @throws IOException if an I/O exception occurs
+ */
+ public void downloadConfig(String configName, Path downloadPath) throws IOException {
+ connect();
+ zkStateReader.getConfigManager().downloadConfigDir(configName, downloadPath);
+ }
+
+ public void uploadConfig(Path configPath, String configName) throws IOException {
+ connect();
+ zkStateReader.getConfigManager().uploadConfigDir(configPath, configName);
+ }
+
+ @Override
+ public void connect() {
+ if (zkStateReader == null) {
+ synchronized (this) {
+ if (zkStateReader == null) {
+ ZkStateReader zk = null;
+ try {
+ zk = new ZkStateReader(zkHost, zkClientTimeout, zkConnectTimeout);
+ zk.createClusterStateWatchersAndUpdate();
+ zkStateReader = zk;
+ log.info("Cluster at {} ready", zkHost);
+ } catch (InterruptedException e) {
+ zk.close();
+ Thread.currentThread().interrupt();
+ throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e);
+ } catch (KeeperException e) {
+ zk.close();
+ throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e);
+ } catch (Exception e) {
+ if (zk != null) zk.close();
+ // do not wrap because clients may be relying on the underlying exception being thrown
+ throw e;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (zkStateReader != null) {
+ synchronized (this) {
+ if (zkStateReader != null)
+ zkStateReader.close();
+ zkStateReader = null;
+ }
+ }
+ }
+
+
+ static String buildZkHostString(Collection<String> zkHosts, String chroot) {
+ if (zkHosts == null || zkHosts.isEmpty()) {
+ throw new IllegalArgumentException("Cannot create CloudSearchClient without valid ZooKeeper host; none specified!");
+ }
+
+ StringBuilder zkBuilder = new StringBuilder();
+ int lastIndexValue = zkHosts.size() - 1;
+ int i = 0;
+ for (String zkHost : zkHosts) {
+ zkBuilder.append(zkHost);
+ if (i < lastIndexValue) {
+ zkBuilder.append(",");
+ }
+ i++;
+ }
+ if (chroot != null) {
+ if (chroot.startsWith("/")) {
+ zkBuilder.append(chroot);
+ } else {
+ throw new IllegalArgumentException(
+ "The chroot must start with a forward slash.");
+ }
+ }
+
+ /* Log the constructed connection string and then initialize. */
+ final String zkHostString = zkBuilder.toString();
+ log.debug("Final constructed zkHost string: " + zkHostString);
+ return zkHostString;
+ }
+
+ @Override
+ public String toString() {
+ return zkHost;
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/d87ffa4b/solr/solrj/src/java/org/apache/solr/common/cloud/ClusterState.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/common/cloud/ClusterState.java b/solr/solrj/src/java/org/apache/solr/common/cloud/ClusterState.java
index 3ab5a1f..302ee62 100644
--- a/solr/solrj/src/java/org/apache/solr/common/cloud/ClusterState.java
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/ClusterState.java
@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
@@ -442,13 +443,19 @@ public class ClusterState implements JSONWriter.Writable {
}
public static class CollectionRef {
+ protected final AtomicInteger gets = new AtomicInteger();
private final DocCollection coll;
+ public int getCount(){
+ return gets.get();
+ }
+
public CollectionRef(DocCollection coll) {
this.coll = coll;
}
public DocCollection get(){
+ gets.incrementAndGet();
return coll;
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/d87ffa4b/solr/solrj/src/java/org/apache/solr/common/cloud/ZkStateReader.java
----------------------------------------------------------------------
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 a858f34..fea5978 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
@@ -633,6 +633,7 @@ public class ZkStateReader implements Closeable {
@Override
public DocCollection get() {
+ gets.incrementAndGet();
// TODO: consider limited caching
return getCollectionLive(ZkStateReader.this, collName);
}
@@ -915,14 +916,18 @@ public class ZkStateReader implements Closeable {
}
return null;
}
-
+
/**
* Returns the baseURL corresponding to a given node's nodeName --
- * NOTE: does not (currently) imply that the nodeName (or resulting
+ * NOTE: does not (currently) imply that the nodeName (or resulting
* baseURL) exists in the cluster.
* @lucene.experimental
*/
public String getBaseUrlForNodeName(final String nodeName) {
+ return getBaseUrlForNodeName(nodeName, getClusterProperty(URL_SCHEME, "http"));
+ }
+
+ public static String getBaseUrlForNodeName(final String nodeName, String urlScheme) {
final int _offset = nodeName.indexOf("_");
if (_offset < 0) {
throw new IllegalArgumentException("nodeName does not contain expected '_' seperator: " + nodeName);
@@ -930,7 +935,6 @@ public class ZkStateReader implements Closeable {
final String hostAndPort = nodeName.substring(0,_offset);
try {
final String path = URLDecoder.decode(nodeName.substring(1+_offset), "UTF-8");
- String urlScheme = getClusterProperty(URL_SCHEME, "http");
return urlScheme + "://" + hostAndPort + (path.isEmpty() ? "" : ("/" + path));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("JVM Does not seem to support UTF-8", e);
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/d87ffa4b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCacheTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCacheTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCacheTest.java
new file mode 100644
index 0000000..70f8dbd
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCacheTest.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.client.solrj.impl;
+
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.http.NoHttpResponseException;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.util.NamedList;
+import org.easymock.EasyMock;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class CloudSolrClientCacheTest extends SolrTestCaseJ4 {
+
+ public void testCaching() throws Exception {
+ String collName = "gettingstarted";
+ Set<String> livenodes = new HashSet<>();
+ Map<String, ClusterState.CollectionRef> refs = new HashMap<>();
+ Map<String, DocCollection> colls = new HashMap<>();
+
+ class Ref extends ClusterState.CollectionRef {
+ private String c;
+
+ public Ref(String c) {
+ super(null);
+ this.c = c;
+ }
+
+ @Override
+ public boolean isLazilyLoaded() {
+ return true;
+ }
+
+ @Override
+ public DocCollection get() {
+ gets.incrementAndGet();
+ return colls.get(c);
+ }
+ }
+ Map<String, Function> responses = new HashMap<>();
+ NamedList okResponse = new NamedList();
+ okResponse.add("responseHeader", new NamedList<>(Collections.singletonMap("status", 0)));
+
+ LBHttpSolrClient mockLbclient = getMockLbHttpSolrClient(responses);
+ AtomicInteger lbhttpRequestCount = new AtomicInteger();
+ try (CloudSolrClient cloudClient = new CloudSolrClient.Builder()
+ .withLBHttpSolrClient(mockLbclient)
+ .withClusterStateProvider(getStateProvider(livenodes, refs))
+
+ .build()) {
+ livenodes.addAll(ImmutableSet.of("192.168.1.108:7574_solr", "192.168.1.108:8983_solr"));
+ ClusterState cs = ClusterState.load(1, coll1State.getBytes(UTF_8),
+ Collections.emptySet(), "/collections/gettingstarted/state.json");
+ refs.put(collName, new Ref(collName));
+ colls.put(collName, cs.getCollectionOrNull(collName));
+ responses.put("request", o -> {
+ int i = lbhttpRequestCount.incrementAndGet();
+ if (i == 1) return new ConnectException("TEST");
+ if (i == 2) return new SocketException("TEST");
+ if (i == 3) return new NoHttpResponseException("TEST");
+ return okResponse;
+ });
+ UpdateRequest update = new UpdateRequest()
+ .add("id", "123", "desc", "Something 0");
+
+ cloudClient.request(update, collName);
+ assertEquals(2, refs.get(collName).getCount());
+ }
+
+ }
+
+
+ private LBHttpSolrClient getMockLbHttpSolrClient(Map<String, Function> responses) throws Exception {
+ LBHttpSolrClient mockLbclient = EasyMock.createMock(LBHttpSolrClient.class);
+ EasyMock.reset(mockLbclient);
+
+ mockLbclient.request(EasyMock.anyObject(LBHttpSolrClient.Req.class));
+ EasyMock.expectLastCall().andAnswer(() -> {
+ LBHttpSolrClient.Req req = (LBHttpSolrClient.Req) EasyMock.getCurrentArguments()[0];
+ Function f = responses.get("request");
+ if (f == null) return null;
+ Object res = f.apply(null);
+ if (res instanceof Exception) throw (Throwable) res;
+ LBHttpSolrClient.Rsp rsp = new LBHttpSolrClient.Rsp();
+ rsp.rsp = (NamedList<Object>) res;
+ rsp.server = req.servers.get(0);
+ return rsp;
+ }).anyTimes();
+
+ mockLbclient.getHttpClient();
+ EasyMock.expectLastCall().andAnswer(() -> null).anyTimes();
+
+ EasyMock.replay(mockLbclient);
+ return mockLbclient;
+ }
+
+ private CloudSolrClient.ClusterStateProvider getStateProvider(Set<String> livenodes,
+ Map<String, ClusterState.CollectionRef> colls) {
+ return new CloudSolrClient.ClusterStateProvider() {
+ @Override
+ public ClusterState.CollectionRef getState(String collection) {
+ return colls.get(collection);
+ }
+
+ @Override
+ public Set<String> liveNodes() {
+ return livenodes;
+ }
+
+ @Override
+ public Map<String, Object> getClusterProperties() {
+ return Collections.EMPTY_MAP;
+ }
+
+ @Override
+ public String getAlias(String collection) {
+ return collection;
+ }
+
+ @Override
+ public String getCollectionName(String name) {
+ return name;
+ }
+
+ @Override
+ public void connect() { }
+
+ @Override
+ public void close() throws IOException {
+
+ }
+ };
+
+ }
+
+
+ private String coll1State = "{'gettingstarted':{\n" +
+ " 'replicationFactor':'2',\n" +
+ " 'router':{'name':'compositeId'},\n" +
+ " 'maxShardsPerNode':'2',\n" +
+ " 'autoAddReplicas':'false',\n" +
+ " 'shards':{\n" +
+ " 'shard1':{\n" +
+ " 'range':'80000000-ffffffff',\n" +
+ " 'state':'active',\n" +
+ " 'replicas':{\n" +
+ " 'core_node2':{\n" +
+ " 'core':'gettingstarted_shard1_replica1',\n" +
+ " 'base_url':'http://192.168.1.108:8983/solr',\n" +
+ " 'node_name':'192.168.1.108:8983_solr',\n" +
+ " 'state':'active',\n" +
+ " 'leader':'true'},\n" +
+ " 'core_node4':{\n" +
+ " 'core':'gettingstarted_shard1_replica2',\n" +
+ " 'base_url':'http://192.168.1.108:7574/solr',\n" +
+ " 'node_name':'192.168.1.108:7574_solr',\n" +
+ " 'state':'active'}}},\n" +
+ " 'shard2':{\n" +
+ " 'range':'0-7fffffff',\n" +
+ " 'state':'active',\n" +
+ " 'replicas':{\n" +
+ " 'core_node1':{\n" +
+ " 'core':'gettingstarted_shard2_replica1',\n" +
+ " 'base_url':'http://192.168.1.108:8983/solr',\n" +
+ " 'node_name':'192.168.1.108:8983_solr',\n" +
+ " 'state':'active',\n" +
+ " 'leader':'true'},\n" +
+ " 'core_node3':{\n" +
+ " 'core':'gettingstarted_shard2_replica2',\n" +
+ " 'base_url':'http://192.168.1.108:7574/solr',\n" +
+ " 'node_name':'192.168.1.108:7574_solr',\n" +
+ " 'state':'active'}}}}}}";
+
+
+}