blob: fbe845bced924e9b75eaeba79c523e80dde0aa6a [file] [log] [blame]
/*
* 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.common.cloud;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.StrUtils;
import org.noggit.JSONWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.apache.solr.common.cloud.DocCollection.DOC_ROUTER;
/**
* Class to partition int range into n ranges.
* @lucene.experimental
*/
public abstract class DocRouter {
public static final String DEFAULT_NAME = CompositeIdRouter.NAME;
public static final DocRouter DEFAULT = new CompositeIdRouter();
public static DocRouter getDocRouter(String routerName) {
DocRouter router = routerMap.get(routerName);
if (router != null) return router;
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown document router '"+ routerName + "'");
}
public String getRouteField(DocCollection coll) {
if (coll == null) return null;
@SuppressWarnings({"rawtypes"})
Map m = (Map) coll.get(DOC_ROUTER);
if (m == null) return null;
return (String) m.get("field");
}
public static Map<String, Object> getRouterSpec(ZkNodeProps props) {
Map<String, Object> map = new LinkedHashMap<>();
for (String s : props.keySet()) {
if (s.startsWith("router.")) {
map.put(s.substring(7), props.get(s));
}
}
if (map.get("name") == null) {
map.put("name", DEFAULT_NAME);
}
return map;
}
// currently just an implementation detail...
private final static Map<String, DocRouter> routerMap;
static {
routerMap = new HashMap<>();
PlainIdRouter plain = new PlainIdRouter();
// instead of doing back compat this way, we could always convert the clusterstate on first read to "plain" if it doesn't have any properties.
routerMap.put(null, plain); // back compat with 4.0
routerMap.put(PlainIdRouter.NAME, plain);
routerMap.put(CompositeIdRouter.NAME, DEFAULT_NAME.equals(CompositeIdRouter.NAME) ? DEFAULT : new CompositeIdRouter());
routerMap.put(ImplicitDocRouter.NAME, new ImplicitDocRouter());
// NOTE: careful that the map keys (the static .NAME members) are filled in by making them final
}
// Hash ranges can't currently "wrap" - i.e. max must be greater or equal to min.
// TODO: ranges may not be all contiguous in the future (either that or we will
// need an extra class to model a collection of ranges)
public static class Range implements JSONWriter.Writable, Comparable<Range> {
public int min; // inclusive
public int max; // inclusive
public Range(int min, int max) {
assert min <= max;
this.min = min;
this.max = max;
}
public int min() {
return min;
}
public int max() {
return max;
}
public boolean includes(int hash) {
return hash >= min && hash <= max;
}
public boolean isSubsetOf(Range superset) {
return superset.min <= min && superset.max >= max;
}
public boolean overlaps(Range other) {
return includes(other.min) || includes(other.max) || isSubsetOf(other);
}
@Override
public String toString() {
return Integer.toHexString(min) + '-' + Integer.toHexString(max);
}
@Override
public int hashCode() {
// difficult numbers to hash... only the highest bits will tend to differ.
// ranges will only overlap during a split, so we can just hash the lower range.
return (min>>28) + (min>>25) + (min>>21) + min;
}
@Override
public boolean equals(Object obj) {
if (obj.getClass() != getClass()) return false;
Range other = (Range)obj;
return this.min == other.min && this.max == other.max;
}
@Override
public void write(JSONWriter writer) {
writer.write(toString());
}
@Override
public int compareTo(Range that) {
int mincomp = Integer.compare(this.min, that.min);
return mincomp == 0 ? Integer.compare(this.max, that.max) : mincomp;
}
}
public Range fromString(String range) {
int middle = range.indexOf('-');
String minS = range.substring(0, middle);
String maxS = range.substring(middle+1);
long min = Long.parseLong(minS, 16); // use long to prevent the parsing routines from potentially worrying about overflow
long max = Long.parseLong(maxS, 16);
return new Range((int)min, (int)max);
}
public Range fullRange() {
return new Range(Integer.MIN_VALUE, Integer.MAX_VALUE);
}
/**
* Split the range into partitions.
* @param partitions number of partitions
* @param range range to split
*/
public List<Range> partitionRange(int partitions, Range range) {
return partitionRange(partitions, range, 0.0f);
}
/**
* Split the range into partitions with inexact sizes.
* @param partitions number of partitions
* @param range range to split
* @param fuzz value between 0 (inclusive) and 0.5 (exclusive) indicating inexact split, i.e. percentage
* of variation in resulting ranges - odd ranges will be larger and even ranges will be smaller
* by up to that percentage.
*/
public List<Range> partitionRange(int partitions, Range range, float fuzz) {
int min = range.min;
int max = range.max;
assert max >= min;
if (fuzz > 0.5f) {
throw new IllegalArgumentException("'fuzz' parameter must be <= 0.5f but was " + fuzz);
} else if (fuzz < 0.0f) {
fuzz = 0.0f;
}
if (partitions == 0) return Collections.emptyList();
long rangeSize = (long)max - (long)min;
long rangeStep = Math.max(1, rangeSize / partitions);
long fuzzStep = Math.round(rangeStep * (double)fuzz / 2.0);
List<Range> ranges = new ArrayList<>(partitions);
long start = min;
long end = start;
boolean odd = true;
while (end < max) {
end = start + rangeStep;
if (fuzzStep > 0) {
if (odd) {
end = end + fuzzStep;
} else {
end = end - fuzzStep;
}
odd = !odd;
}
// make last range always end exactly on MAX_VALUE
if (ranges.size() == partitions - 1) {
end = max;
}
ranges.add(new Range((int)start, (int)end));
start = end + 1L;
}
return ranges;
}
/** Returns the Slice that the document should reside on, or null if there is not enough information */
public abstract Slice getTargetSlice(String id, SolrInputDocument sdoc, String route, SolrParams params, DocCollection collection);
/** This method is consulted to determine what slices should be queried for a request when
* an explicit shards parameter was not used.
* This method only accepts a single shard key (or null). If you have a comma separated list of shard keys,
* call getSearchSlices
**/
public abstract Collection<Slice> getSearchSlicesSingle(String shardKey, SolrParams params, DocCollection collection);
/** This method is consulted to determine what search range (the part of the hash ring) should be queried for a request when
* an explicit shards parameter was not used.
* This method only accepts a single shard key (or null).
*/
public Range getSearchRangeSingle(String shardKey, SolrParams params, DocCollection collection) {
throw new UnsupportedOperationException();
}
public abstract boolean isTargetSlice(String id, SolrInputDocument sdoc, SolrParams params, String shardId, DocCollection collection);
public abstract String getName();
/** This method is consulted to determine what slices should be queried for a request when
* an explicit shards parameter was not used.
* This method accepts a multi-valued shardKeys parameter (normally comma separated from the shard.keys request parameter)
* and aggregates the slices returned by getSearchSlicesSingle for each shardKey.
**/
public Collection<Slice> getSearchSlices(String shardKeys, SolrParams params, DocCollection collection) {
if (shardKeys == null || shardKeys.indexOf(',') < 0) {
return getSearchSlicesSingle(shardKeys, params, collection);
}
List<String> shardKeyList = StrUtils.splitSmart(shardKeys, ",", true);
HashSet<Slice> allSlices = new HashSet<>();
for (String shardKey : shardKeyList) {
allSlices.addAll( getSearchSlicesSingle(shardKey, params, collection) );
}
return allSlices;
}
}