SOLR-8332: Factor HttpShardHandler[Factory]'s url shuffling out into a ReplicaListTransformer class.

(Christine Poerschke, Noble Paul)
This commit is contained in:
Christine Poerschke 2016-11-09 09:46:14 +00:00
parent 98b8370473
commit 6c25adb119
7 changed files with 360 additions and 29 deletions

View File

@ -156,6 +156,9 @@ Other Changes
* SOLR-9739: JavabinCodec implements PushWriter interface (noble) * SOLR-9739: JavabinCodec implements PushWriter interface (noble)
* SOLR-8332: Factor HttpShardHandler[Factory]'s url shuffling out into a ReplicaListTransformer class.
(Christine Poerschke, Noble Paul)
================== 6.3.0 ================== ================== 6.3.0 ==================
Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release. Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release.

View File

@ -17,6 +17,7 @@
package org.apache.solr.handler.component; package org.apache.solr.handler.component;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.net.ConnectException; import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@ -116,7 +117,7 @@ public class HttpShardHandler extends ShardHandler {
private List<String> getURLs(String shard, String preferredHostAddress) { private List<String> getURLs(String shard, String preferredHostAddress) {
List<String> urls = shardToURLs.get(shard); List<String> urls = shardToURLs.get(shard);
if (urls == null) { if (urls == null) {
urls = httpShardHandlerFactory.makeURLList(shard); urls = httpShardHandlerFactory.buildURLList(shard);
if (preferredHostAddress != null && urls.size() > 1) { if (preferredHostAddress != null && urls.size() > 1) {
preferCurrentHostForDistributedReq(preferredHostAddress, urls); preferCurrentHostForDistributedReq(preferredHostAddress, urls);
} }
@ -320,6 +321,8 @@ public class HttpShardHandler extends ShardHandler {
} }
} }
final ReplicaListTransformer replicaListTransformer = httpShardHandlerFactory.getReplicaListTransformer(req);
if (shards != null) { if (shards != null) {
List<String> lst = StrUtils.splitSmart(shards, ",", true); List<String> lst = StrUtils.splitSmart(shards, ",", true);
rb.shards = lst.toArray(new String[lst.size()]); rb.shards = lst.toArray(new String[lst.size()]);
@ -404,7 +407,11 @@ public class HttpShardHandler extends ShardHandler {
for (int i=0; i<rb.shards.length; i++) { for (int i=0; i<rb.shards.length; i++) {
if (rb.shards[i] == null) { final List<String> shardUrls;
if (rb.shards[i] != null) {
shardUrls = StrUtils.splitSmart(rb.shards[i], "|", true);
replicaListTransformer.transform(shardUrls);
} else {
if (clusterState == null) { if (clusterState == null) {
clusterState = zkController.getClusterState(); clusterState = zkController.getClusterState();
slices = clusterState.getSlicesMap(cloudDescriptor.getCollectionName()); slices = clusterState.getSlicesMap(cloudDescriptor.getCollectionName());
@ -421,26 +428,25 @@ public class HttpShardHandler extends ShardHandler {
// throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "no such shard: " + sliceName); // throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "no such shard: " + sliceName);
} }
Map<String, Replica> sliceShards = slice.getReplicasMap(); final Collection<Replica> allSliceReplicas = slice.getReplicasMap().values();
final List<Replica> eligibleSliceReplicas = new ArrayList<>(allSliceReplicas.size());
// For now, recreate the | delimited list of equivalent servers for (Replica replica : allSliceReplicas) {
StringBuilder sliceShardsStr = new StringBuilder();
boolean first = true;
for (Replica replica : sliceShards.values()) {
if (!clusterState.liveNodesContain(replica.getNodeName()) if (!clusterState.liveNodesContain(replica.getNodeName())
|| replica.getState() != Replica.State.ACTIVE) { || replica.getState() != Replica.State.ACTIVE) {
continue; continue;
} }
if (first) { eligibleSliceReplicas.add(replica);
first = false;
} else {
sliceShardsStr.append('|');
}
String url = ZkCoreNodeProps.getCoreUrl(replica);
sliceShardsStr.append(url);
} }
if (sliceShardsStr.length() == 0) { replicaListTransformer.transform(eligibleSliceReplicas);
shardUrls = new ArrayList<>(eligibleSliceReplicas.size());
for (Replica replica : eligibleSliceReplicas) {
String url = ZkCoreNodeProps.getCoreUrl(replica);
shardUrls.add(url);
}
if (shardUrls.isEmpty()) {
boolean tolerant = rb.req.getParams().getBool(ShardParams.SHARDS_TOLERANT, false); boolean tolerant = rb.req.getParams().getBool(ShardParams.SHARDS_TOLERANT, false);
if (!tolerant) { if (!tolerant) {
// stop the check when there are no replicas available for a shard // stop the check when there are no replicas available for a shard
@ -448,9 +454,19 @@ public class HttpShardHandler extends ShardHandler {
"no servers hosting shard: " + rb.slices[i]); "no servers hosting shard: " + rb.slices[i]);
} }
} }
rb.shards[i] = sliceShardsStr.toString();
} }
// And now recreate the | delimited list of equivalent servers
final StringBuilder sliceShardsStr = new StringBuilder();
boolean first = true;
for (String shardUrl : shardUrls) {
if (first) {
first = false;
} else {
sliceShardsStr.append('|');
}
sliceShardsStr.append(shardUrl);
}
rb.shards[i] = sliceShardsStr.toString();
} }
} }
String shards_rows = params.get(ShardParams.SHARDS_ROWS); String shards_rows = params.get(ShardParams.SHARDS_ROWS);

View File

@ -31,13 +31,13 @@ import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.URLUtil; import org.apache.solr.common.util.URLUtil;
import org.apache.solr.core.PluginInfo; import org.apache.solr.core.PluginInfo;
import org.apache.solr.update.UpdateShardHandlerConfig; import org.apache.solr.update.UpdateShardHandlerConfig;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.util.DefaultSolrThreadFactory; import org.apache.solr.util.DefaultSolrThreadFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
@ -84,6 +84,8 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
private final Random r = new Random(); private final Random r = new Random();
private final ReplicaListTransformer shufflingReplicaListTransformer = new ShufflingReplicaListTransformer(r);
// URL scheme to be used in distributed search. // URL scheme to be used in distributed search.
static final String INIT_URL_SCHEME = "urlScheme"; static final String INIT_URL_SCHEME = "urlScheme";
@ -227,12 +229,12 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
} }
/** /**
* Creates a randomized list of urls for the given shard. * Creates a list of urls for the given shard.
* *
* @param shard the urls for the shard, separated by '|' * @param shard the urls for the shard, separated by '|'
* @return A list of valid urls (including protocol) that are replicas for the shard * @return A list of valid urls (including protocol) that are replicas for the shard
*/ */
public List<String> makeURLList(String shard) { public List<String> buildURLList(String shard) {
List<String> urls = StrUtils.splitSmart(shard, "|", true); List<String> urls = StrUtils.splitSmart(shard, "|", true);
// convert shard to URL // convert shard to URL
@ -240,17 +242,14 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory implements org.
urls.set(i, buildUrl(urls.get(i))); urls.set(i, buildUrl(urls.get(i)));
} }
//
// Shuffle the list instead of use round-robin by default.
// This prevents accidental synchronization where multiple shards could get in sync
// and query the same replica at the same time.
//
if (urls.size() > 1)
Collections.shuffle(urls, r);
return urls; return urls;
} }
ReplicaListTransformer getReplicaListTransformer(final SolrQueryRequest req)
{
return shufflingReplicaListTransformer;
}
/** /**
* Creates a new completion service for use by a single set of distributed requests. * Creates a new completion service for use by a single set of distributed requests.
*/ */

View File

@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.component;
import java.util.List;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.params.ShardParams;
interface ReplicaListTransformer {
/**
* Transforms the passed in list of choices. Transformations can include (but are not limited to)
* reordering of elements (e.g. via shuffling) and removal of elements (i.e. filtering).
*
* @param choices - a list of choices to transform, typically the choices are {@link Replica} objects but choices
* can also be {@link String} objects such as URLs passed in via the {@link ShardParams#SHARDS} parameter.
*/
public void transform(List<?> choices);
}

View File

@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.component;
import java.util.Collections;
import java.util.List;
import java.util.Random;
class ShufflingReplicaListTransformer implements ReplicaListTransformer {
private final Random r;
public ShufflingReplicaListTransformer(Random r)
{
this.r = r;
}
public void transform(List<?> choices)
{
if (choices.size() > 1) {
Collections.shuffle(choices, r);
}
}
}

View File

@ -0,0 +1,163 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.junit.Test;
public class ReplicaListTransformerTest extends LuceneTestCase {
// A transformer that keeps only matching choices
private static class ToyMatchingReplicaListTransformer implements ReplicaListTransformer {
private final String regex;
public ToyMatchingReplicaListTransformer(String regex)
{
this.regex = regex;
}
public void transform(List<?> choices)
{
Iterator<?> it = choices.iterator();
while (it.hasNext()) {
Object choice = it.next();
final String url;
if (choice instanceof String) {
url = (String)choice;
}
else if (choice instanceof Replica) {
url = ((Replica)choice).getCoreUrl();
} else {
url = null;
}
if (url == null || !url.matches(regex)) {
it.remove();
}
}
}
}
// A transformer that makes no transformation
private static class ToyNoOpReplicaListTransformer implements ReplicaListTransformer {
public ToyNoOpReplicaListTransformer()
{
}
public void transform(List<?> choices)
{
// no-op
}
}
@Test
public void testTransform() throws Exception {
final String regex = ".*" + random().nextInt(10) + ".*";
final ReplicaListTransformer transformer;
if (random().nextBoolean()) {
transformer = new ToyMatchingReplicaListTransformer(regex);
} else {
transformer = new HttpShardHandlerFactory() {
@Override
ReplicaListTransformer getReplicaListTransformer(final SolrQueryRequest req)
{
final SolrParams params = req.getParams();
if (params.getBool("toyNoTransform", false)) {
return new ToyNoOpReplicaListTransformer();
}
final String regex = params.get("toyRegEx");
if (regex != null) {
return new ToyMatchingReplicaListTransformer(regex);
}
return super.getReplicaListTransformer(req);
}
}.getReplicaListTransformer(
new LocalSolrQueryRequest(null,
new ModifiableSolrParams().add("toyRegEx", regex)));
}
final List<Replica> inputs = new ArrayList<>();
final List<Replica> expectedTransformed = new ArrayList<>();
final List<String> urls = createRandomUrls();
for (int ii=0; ii<urls.size(); ++ii) {
final String name = "replica"+(ii+1);
final String url = urls.get(ii);
final Map<String,Object> propMap = new HashMap<String,Object>();
propMap.put("base_url", url);
// a skeleton replica, good enough for this test's purposes
final Replica replica = new Replica(name, propMap);
inputs.add(replica);
if (url.matches(regex)) {
expectedTransformed.add(replica);
}
}
final List<Replica> actualTransformed = new ArrayList<>(inputs);
transformer.transform(actualTransformed);
assertEquals(expectedTransformed.size(), actualTransformed.size());
for (int ii=0; ii<expectedTransformed.size(); ++ii) {
assertEquals("mismatch for ii="+ii, expectedTransformed.get(ii), actualTransformed.get(ii));
}
}
private final List<String> createRandomUrls() throws Exception {
final List<String> urls = new ArrayList<>();
maybeAddUrl(urls, "a"+random().nextDouble());
maybeAddUrl(urls, "bb"+random().nextFloat());
maybeAddUrl(urls, "ccc"+random().nextGaussian());
maybeAddUrl(urls, "dddd"+random().nextInt());
maybeAddUrl(urls, "eeeee"+random().nextLong());
Collections.shuffle(urls, random());
return urls;
}
private final void maybeAddUrl(final List<String> urls, final String url) {
if (random().nextBoolean()) {
urls.add(url);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.common.cloud.Replica;
import org.junit.Test;
public class ShufflingReplicaListTransformerTest extends LuceneTestCase {
private final ShufflingReplicaListTransformer transformer = new ShufflingReplicaListTransformer(random());
@Test
public void testTransformReplicas() throws Exception {
final List<Replica> replicas = new ArrayList<>();
for (final String url : createRandomUrls()) {
replicas.add(new Replica(url, new HashMap<String,Object>()));
}
implTestTransform(replicas);
}
@Test
public void testTransformUrls() throws Exception {
final List<String> urls = createRandomUrls();
implTestTransform(urls);
}
private <TYPE> void implTestTransform(List<TYPE> inputs) throws Exception {
final List<TYPE> transformedInputs = new ArrayList<>(inputs);
transformer.transform(transformedInputs);
final Set<TYPE> inputSet = new HashSet<>(inputs);
final Set<TYPE> transformedSet = new HashSet<>(transformedInputs);
assertTrue(inputSet.equals(transformedSet));
}
private final List<String> createRandomUrls() throws Exception {
final List<String> urls = new ArrayList<>();
maybeAddUrl(urls, "a"+random().nextDouble());
maybeAddUrl(urls, "bb"+random().nextFloat());
maybeAddUrl(urls, "ccc"+random().nextGaussian());
maybeAddUrl(urls, "dddd"+random().nextInt());
maybeAddUrl(urls, "eeeee"+random().nextLong());
Collections.shuffle(urls, random());
return urls;
}
private final void maybeAddUrl(final List<String> urls, final String url) {
if (random().nextBoolean()) {
urls.add(url);
}
}
}