| /* |
| * 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.cloud.autoscaling; |
| |
| import java.io.IOException; |
| import java.io.StringWriter; |
| import java.lang.invoke.MethodHandles; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import java.util.function.Supplier; |
| import java.util.stream.Collectors; |
| |
| import org.apache.solr.client.solrj.cloud.NodeStateProvider; |
| import org.apache.solr.client.solrj.cloud.SolrCloudManager; |
| import org.apache.solr.client.solrj.cloud.autoscaling.Variable.Type; |
| import org.apache.solr.client.solrj.impl.ClusterStateProvider; |
| import org.apache.solr.common.IteratorWriter; |
| import org.apache.solr.common.MapWriter; |
| import org.apache.solr.common.cloud.ClusterState; |
| import org.apache.solr.common.cloud.rule.ImplicitSnitch; |
| import org.apache.solr.common.params.CollectionAdminParams; |
| import org.apache.solr.common.params.CollectionParams.CollectionAction; |
| import org.apache.solr.common.util.Pair; |
| import org.apache.solr.common.util.StrUtils; |
| import org.apache.solr.common.util.Utils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static java.util.Collections.emptyList; |
| import static java.util.Collections.emptyMap; |
| import static java.util.stream.Collectors.collectingAndThen; |
| import static java.util.stream.Collectors.toList; |
| import static org.apache.solr.client.solrj.cloud.autoscaling.Variable.Type.NODE; |
| import static org.apache.solr.client.solrj.cloud.autoscaling.Variable.Type.WITH_COLLECTION; |
| |
| /** |
| * The class that reads, parses and applies policies specified in |
| * autoscaling.json |
| * |
| * Create one instance of this class per unique autoscaling.json. |
| * This is immutable and is thread-safe |
| * |
| * Create a fresh new session for each use. |
| * |
| * @deprecated to be removed in Solr 9.0 (see SOLR-14656) |
| */ |
| public class Policy implements MapWriter { |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| |
| public static final String POLICY = "policy"; |
| public static final String EACH = "#EACH"; |
| public static final String ANY = "#ANY"; |
| public static final String POLICIES = "policies"; |
| public static final String CLUSTER_POLICY = "cluster-policy"; |
| public static final String CLUSTER_PREFERENCES = "cluster-preferences"; |
| public static final Set<String> GLOBAL_ONLY_TAGS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("cores", CollectionAdminParams.WITH_COLLECTION))); |
| @SuppressWarnings({"unchecked"}) |
| public static final List<Preference> DEFAULT_PREFERENCES = Collections.unmodifiableList( |
| Arrays.asList( |
| // NOTE - if you change this, make sure to update the solrcloud-autoscaling-overview.adoc which |
| // lists the default preferences |
| new Preference((Map<String, Object>) Utils.fromJSONString("{minimize : cores, precision:1}")), |
| new Preference((Map<String, Object>) Utils.fromJSONString("{maximize : freedisk}")))); |
| |
| /** |
| * These parameters are always fetched for all nodes regardless of whether they are used in preferences or not |
| */ |
| private static final List<String> DEFAULT_PARAMS_OF_INTEREST = Arrays.asList(ImplicitSnitch.DISK, ImplicitSnitch.CORES); |
| |
| private final Map<String, List<Clause>> policies; |
| private final List<Clause> clusterPolicy; |
| private final List<Preference> clusterPreferences; |
| private final List<Pair<String, Type>> params; |
| private final List<String> perReplicaAttributes; |
| private final int zkVersion; |
| /** |
| * True if cluster policy, preferences and custom policies are all non-existent |
| */ |
| private final boolean empty; |
| /** |
| * True if cluster preferences was originally empty, false otherwise. It is used to figure out if |
| * the current preferences were implicitly added or not. |
| */ |
| private final boolean emptyPreferences; |
| |
| public Policy() { |
| this(Collections.emptyMap()); |
| } |
| |
| public Policy(Map<String, Object> jsonMap) { |
| this(jsonMap, 0); |
| } |
| @SuppressWarnings("unchecked") |
| public Policy(Map<String, Object> jsonMap, int version) { |
| this.empty = jsonMap.get(CLUSTER_PREFERENCES) == null && jsonMap.get(CLUSTER_POLICY) == null && jsonMap.get(POLICIES) == null; |
| this.zkVersion = version; |
| int[] idx = new int[1]; |
| List<Preference> initialClusterPreferences = ((List<Map<String, Object>>) jsonMap.getOrDefault(CLUSTER_PREFERENCES, emptyList())).stream() |
| .map(m -> new Preference(m, idx[0]++)) |
| .collect(toList()); |
| for (int i = 0; i < initialClusterPreferences.size() - 1; i++) { |
| Preference preference = initialClusterPreferences.get(i); |
| preference.next = initialClusterPreferences.get(i + 1); |
| } |
| emptyPreferences = initialClusterPreferences.isEmpty(); |
| if (emptyPreferences) { |
| initialClusterPreferences.addAll(DEFAULT_PREFERENCES); |
| } |
| this.clusterPreferences = Collections.unmodifiableList(initialClusterPreferences); |
| final SortedSet<String> paramsOfInterest = new TreeSet<>(DEFAULT_PARAMS_OF_INTEREST); |
| clusterPreferences.forEach(preference -> paramsOfInterest.add(preference.name.toString())); |
| List<String> newParams = new ArrayList<>(paramsOfInterest); |
| clusterPolicy = ((List<Map<String, Object>>) jsonMap.getOrDefault(CLUSTER_POLICY, emptyList())).stream() |
| .map(Clause::create) |
| .filter(clause -> { |
| clause.addTags(newParams); |
| return true; |
| }) |
| .collect(collectingAndThen(toList(), Collections::unmodifiableList)); |
| |
| for (String newParam : new ArrayList<>(newParams)) { |
| Type t = VariableBase.getTagType(newParam); |
| if(t != null && !t.associatedPerNodeValues.isEmpty()){ |
| for (String s : t.associatedPerNodeValues) { |
| if(!newParams.contains(s)) newParams.add(s); |
| } |
| } |
| } |
| |
| this.policies = Collections.unmodifiableMap( |
| clausesFromMap((Map<String, List<Map<String, Object>>>) jsonMap.getOrDefault(POLICIES, emptyMap()), newParams)); |
| List<Pair<String, Type>> params = newParams.stream() |
| .map(s -> new Pair<>(s, VariableBase.getTagType(s))) |
| .collect(toList()); |
| //let this be there always, there is no extra cost |
| params.add(new Pair<>(WITH_COLLECTION.tagName, WITH_COLLECTION)); |
| this.params = Collections.unmodifiableList(params); |
| perReplicaAttributes = readPerReplicaAttrs(); |
| } |
| |
| private List<String> readPerReplicaAttrs() { |
| return this.params.stream() |
| .map(s -> s.second().perReplicaValue) |
| .filter(Objects::nonNull) |
| .collect(Collectors.toList()); |
| } |
| |
| private Policy(Map<String, List<Clause>> policies, List<Clause> clusterPolicy, List<Preference> clusterPreferences, int version) { |
| this.empty = policies == null && clusterPolicy == null && clusterPreferences == null; |
| this.zkVersion = version; |
| this.policies = policies != null ? Collections.unmodifiableMap(policies) : Collections.emptyMap(); |
| this.clusterPolicy = clusterPolicy != null ? Collections.unmodifiableList(clusterPolicy) : Collections.emptyList(); |
| this.emptyPreferences = clusterPreferences == null; |
| this.clusterPreferences = emptyPreferences ? DEFAULT_PREFERENCES : Collections.unmodifiableList(clusterPreferences); |
| this.params = Collections.unmodifiableList( |
| buildParams(this.clusterPreferences, this.clusterPolicy, this.policies).stream() |
| .map(s -> new Pair<>(s, VariableBase.getTagType(s))) |
| .collect(toList()) |
| ); |
| perReplicaAttributes = readPerReplicaAttrs(); |
| } |
| |
| private List<String> buildParams(List<Preference> preferences, List<Clause> policy, Map<String, List<Clause>> policies) { |
| final SortedSet<String> paramsOfInterest = new TreeSet<>(); |
| preferences.forEach(p -> { |
| if (paramsOfInterest.contains(p.name.name())) { |
| throw new RuntimeException(p.name + " is repeated"); |
| } |
| paramsOfInterest.add(p.name.toString()); |
| }); |
| List<String> newParams = new ArrayList<>(paramsOfInterest); |
| policy.forEach(c -> c.addTags(newParams)); |
| policies.values().forEach(clauses -> clauses.forEach(c -> c.addTags(newParams))); |
| return newParams; |
| } |
| |
| public Policy withPolicies(Map<String, List<Clause>> policies) { |
| return new Policy(policies, clusterPolicy, clusterPreferences, 0); |
| } |
| |
| public Policy withClusterPreferences(List<Preference> clusterPreferences) { |
| return new Policy(policies, clusterPolicy, clusterPreferences, 0); |
| } |
| |
| public Policy withClusterPolicy(List<Clause> clusterPolicy) { |
| return new Policy(policies, clusterPolicy, clusterPreferences, 0); |
| } |
| |
| public Policy withParams(List<String> params) { |
| return new Policy(policies, clusterPolicy, clusterPreferences, 0); |
| } |
| |
| public List<Clause> getClusterPolicy() { |
| return Collections.unmodifiableList(clusterPolicy); |
| } |
| |
| public List<Preference> getClusterPreferences() { |
| return Collections.unmodifiableList(clusterPreferences); |
| } |
| |
| @Override |
| public void writeMap(EntryWriter ew) throws IOException { |
| // if we were initially empty then we don't want to persist any implicitly added |
| // policy or preferences |
| if (empty) return; |
| |
| if (!policies.isEmpty()) { |
| ew.put(POLICIES, (MapWriter) ew1 -> { |
| for (Map.Entry<String, List<Clause>> e : policies.entrySet()) { |
| ew1.put(e.getKey(), e.getValue()); |
| } |
| }); |
| } |
| if (!emptyPreferences && !clusterPreferences.isEmpty()) { |
| ew.put(CLUSTER_PREFERENCES, (IteratorWriter) iw -> { |
| for (Preference p : clusterPreferences) iw.add(p); |
| }); |
| } |
| if (!clusterPolicy.isEmpty()) { |
| ew.put(CLUSTER_POLICY, (IteratorWriter) iw -> { |
| for (Clause c : clusterPolicy) { |
| iw.add(c); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| Policy policy = (Policy) o; |
| |
| if (!getPolicies().equals(policy.getPolicies())) return false; |
| if (!getClusterPolicy().equals(policy.getClusterPolicy())) return false; |
| return getClusterPreferences().equals(policy.getClusterPreferences()); |
| } |
| |
| @Override |
| public int hashCode() { return Objects.hash(getPolicies()); } |
| |
| public static Map<String, List<Clause>> clausesFromMap(Map<String, List<Map<String, Object>>> map, List<String> newParams) { |
| Map<String, List<Clause>> newPolicies = new HashMap<>(); |
| map.forEach((s, l1) -> |
| newPolicies.put(s, l1.stream() |
| .map(Clause::create) |
| .filter(clause -> { |
| if (!clause.isPerCollectiontag()) |
| throw new RuntimeException(clause.getGlobalTag().name + " is only allowed in 'cluster-policy'"); |
| clause.addTags(newParams); |
| return true; |
| }) |
| .sorted() |
| .collect(collectingAndThen(toList(), Collections::unmodifiableList)))); |
| return newPolicies; |
| } |
| |
| static void setApproxValuesAndSortNodes(List<Preference> clusterPreferences, List<Row> matrix) { |
| List<Row> matrixCopy = new ArrayList<>(matrix); |
| List<Row> deadNodes = null; |
| Iterator<Row> it =matrix.iterator(); |
| while (it.hasNext()){ |
| Row row = it.next(); |
| if(!row.isLive){ |
| if(deadNodes == null) deadNodes = new ArrayList<>(); |
| deadNodes.add(row); |
| it.remove(); |
| } |
| } |
| |
| if (!clusterPreferences.isEmpty()) { |
| //this is to set the approximate value according to the precision |
| ArrayList<Row> tmpMatrix = new ArrayList<>(matrix); |
| Row[] lastComparison = new Row[2]; |
| for (Preference p : clusterPreferences) { |
| try { |
| tmpMatrix.sort((r1, r2) -> { |
| lastComparison[0] = r1; |
| lastComparison[1] = r2; |
| return p.compare(r1, r2, false); |
| }); |
| } catch (Exception e) { |
| try { |
| @SuppressWarnings({"rawtypes"}) |
| Map m = Collections.singletonMap("diagnostics", (MapWriter) ew -> { |
| PolicyHelper.writeNodes(ew, matrixCopy); |
| ew.put("config", matrix.get(0).session.getPolicy()); |
| }); |
| log.error("Exception! prefs = {}, recent r1 = {}, r2 = {}, matrix = {}", |
| clusterPreferences, |
| lastComparison[0].node, |
| lastComparison[1].node, |
| Utils.writeJson(m, new StringWriter(), true).toString()); |
| } catch (IOException e1) { |
| // |
| } |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| p.setApproxVal(tmpMatrix); |
| } |
| // the tmpMatrix was needed only to set the approximate values, now we sort the real matrix |
| // recursing through each preference |
| matrix.sort((Row r1, Row r2) -> { |
| int result = clusterPreferences.get(0).compare(r1, r2, true); |
| if (result == 0) result = clusterPreferences.get(0).compare(r1, r2, false); |
| return result; |
| }); |
| |
| if(deadNodes != null){ |
| for (Row deadNode : deadNodes) { |
| matrix.add(0, deadNode); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Insert the collection name into the clauses where collection is not specified |
| */ |
| static List<Clause> insertColl(String coll, Collection<Clause> conditions) { |
| return conditions.stream() |
| .filter(Clause::isPerCollectiontag) |
| .map(clause -> { |
| Map<String, Object> copy = new LinkedHashMap<>(clause.original); |
| if (!copy.containsKey("collection")) { |
| copy.put("collection", coll); |
| copy.put(Clause.class.getName(), clause); |
| } |
| return Clause.create(copy); |
| }) |
| .filter(it -> (it.getCollection().isPass(coll))) |
| .collect(Collectors.toList()); |
| |
| } |
| |
| public Session createSession(SolrCloudManager cloudManager) { |
| return createSession(cloudManager, null); |
| } |
| |
| public Session createSession(SolrCloudManager cloudManager, Transaction tx) { |
| return new Session(cloudManager, this, tx); |
| } |
| |
| public enum SortParam { |
| freedisk(0, Integer.MAX_VALUE), cores(0, Integer.MAX_VALUE), heapUsage(0, Integer.MAX_VALUE), sysLoadAvg(0, 100); |
| |
| public final int min, max; |
| |
| SortParam(int min, int max) { |
| this.min = min; |
| this.max = max; |
| } |
| |
| static SortParam get(String m) { |
| for (SortParam p : values()) if (p.name().equals(m)) return p; |
| throw new RuntimeException(StrUtils.formatString("Invalid sort {0} Sort must be on one of these {1}", m, Arrays.asList(values()))); |
| } |
| } |
| |
| enum Sort { |
| maximize(1), minimize(-1); |
| final int sortval; |
| |
| Sort(int i) { |
| sortval = i; |
| } |
| |
| static Sort get(Map<String, Object> m) { |
| if (m.containsKey(maximize.name()) && m.containsKey(minimize.name())) { |
| throw new RuntimeException("Cannot have both 'maximize' and 'minimize'"); |
| } |
| if (m.containsKey(maximize.name())) return maximize; |
| if (m.containsKey(minimize.name())) return minimize; |
| throw new RuntimeException("must have either 'maximize' or 'minimize'"); |
| } |
| } |
| |
| public static List<Clause> mergePolicies(String coll, |
| List<Clause> collPolicy, |
| List<Clause> globalPolicy) { |
| |
| List<Clause> merged = insertColl(coll, collPolicy); |
| List<Clause> global = insertColl(coll, globalPolicy); |
| merged.addAll(global.stream() |
| .filter(clusterPolicyClause -> merged.stream().noneMatch(perCollPolicy -> perCollPolicy.doesOverride(clusterPolicyClause))) |
| .collect(Collectors.toList())); |
| return merged; |
| } |
| |
| static class Transaction { |
| private final Policy policy; |
| private boolean open = false; |
| private Session firstSession; |
| private Session currentSession; |
| |
| |
| public Transaction(Policy config) { |
| this.policy = config; |
| |
| } |
| |
| public Session open(SolrCloudManager cloudManager) { |
| firstSession = currentSession = policy.createSession(cloudManager, Transaction.this); |
| open = true; |
| return firstSession; |
| } |
| |
| |
| public boolean isOpen() { |
| return open; |
| } |
| |
| List<Violation> close() { |
| if (!open) throw new RuntimeException("Already closed"); |
| open = false; |
| return currentSession.getViolations(); |
| } |
| |
| public Session getCurrentSession() { |
| return currentSession; |
| } |
| |
| void updateSession(Session session) { |
| currentSession = session; |
| } |
| } |
| |
| private static final Map<CollectionAction, Supplier<Suggester>> ops = new HashMap<>(); |
| |
| static { |
| ops.put(CollectionAction.ADDREPLICA, AddReplicaSuggester::new); |
| ops.put(CollectionAction.DELETEREPLICA, DeleteReplicaSuggester::new); |
| ops.put(CollectionAction.DELETENODE, DeleteNodeSuggester::new); |
| ops.put(CollectionAction.MOVEREPLICA, MoveReplicaSuggester::new); |
| ops.put(CollectionAction.SPLITSHARD, SplitShardSuggester::new); |
| ops.put(CollectionAction.MERGESHARDS, () -> new UnsupportedSuggester(CollectionAction.MERGESHARDS)); |
| ops.put(CollectionAction.NONE, () -> new UnsupportedSuggester(CollectionAction.NONE)); |
| } |
| |
| public Map<String, List<Clause>> getPolicies() { |
| return policies; |
| } |
| |
| public List<Pair<String, Type>> getParams() { |
| return Collections.unmodifiableList(params); |
| } |
| |
| public List<String> getParamNames() { |
| return params.stream().map(Pair::first).collect(toList()); |
| } |
| |
| public List<String> getPerReplicaAttributes() { |
| return Collections.unmodifiableList(perReplicaAttributes); |
| } |
| |
| public int getZkVersion() { |
| return zkVersion; |
| } |
| |
| /** |
| * Compares two {@link Row} loads according to a policy. |
| * |
| * @param r1 the first {@link Row} to compare |
| * @param r2 the second {@link Row} to compare |
| * @return the value {@code 0} if r1 and r2 are equally loaded |
| * a value {@code -1} if r1 is more loaded than r2 |
| * a value {@code 1} if r1 is less loaded than r2 |
| */ |
| static int compareRows(Row r1, Row r2, Policy policy) { |
| return policy.clusterPreferences.get(0).compare(r1, r2, true); |
| } |
| |
| @Override |
| public String toString() { |
| return Utils.toJSONString(this); |
| } |
| |
| public boolean isEmpty() { |
| return empty; |
| } |
| |
| /** |
| * @return true if no preferences were specified by the user, false otherwise |
| */ |
| public boolean isEmptyPreferences() { |
| return emptyPreferences; |
| } |
| |
| /*This stores the logical state of the system, given a policy and |
| * a cluster state. |
| * |
| */ |
| public static class Session implements MapWriter { |
| final List<String> nodes; |
| final SolrCloudManager cloudManager; |
| final List<Row> matrix; |
| final NodeStateProvider nodeStateProvider; |
| Set<String> collections = new HashSet<>(); |
| final Policy policy; |
| List<Clause> expandedClauses; |
| List<Violation> violations = new ArrayList<>(); |
| Transaction transaction; |
| |
| |
| /** |
| * This constructor creates a Session from the current Zookeeper collection, replica and node states. |
| */ |
| Session(SolrCloudManager cloudManager, Policy policy, Transaction transaction) { |
| collections = new HashSet<>(); |
| this.transaction = transaction; |
| this.policy = policy; |
| ClusterState state = null; |
| this.nodeStateProvider = cloudManager.getNodeStateProvider(); |
| try { |
| state = cloudManager.getClusterStateProvider().getClusterState(); |
| log.trace("-- session created with cluster state: {}", state); |
| } catch (Exception e) { |
| log.trace("-- session created, can't obtain cluster state", e); |
| } |
| this.nodes = new ArrayList<>(cloudManager.getClusterStateProvider().getLiveNodes()); |
| this.cloudManager = cloudManager; |
| for (String node : nodes) { |
| collections.addAll(nodeStateProvider.getReplicaInfo(node, Collections.emptyList()).keySet()); |
| } |
| |
| expandedClauses = policy.getClusterPolicy().stream() |
| .filter(clause -> !clause.isPerCollectiontag()) |
| .collect(Collectors.toList()); |
| |
| if (nodes.size() > 0) { |
| //if any collection has 'withCollection' irrespective of the node, the NodeStateProvider returns a map value |
| Map<String, Object> vals = nodeStateProvider.getNodeValues(nodes.get(0), Collections.singleton("withCollection")); |
| if (!vals.isEmpty() && vals.get("withCollection") != null) { |
| @SuppressWarnings({"unchecked"}) |
| Map<String, String> withCollMap = (Map<String, String>) vals.get("withCollection"); |
| if (!withCollMap.isEmpty()) { |
| @SuppressWarnings({"unchecked"}) |
| Clause withCollClause = new Clause((Map<String,Object>)Utils.fromJSONString("{withCollection:'*' , node: '#ANY'}") , |
| new Condition(NODE.tagName, "#ANY", Operand.EQUAL, null, null), |
| new Condition(WITH_COLLECTION.tagName,"*" , Operand.EQUAL, null, null), true, null, false |
| ); |
| expandedClauses.add(withCollClause); |
| } |
| } |
| } |
| |
| ClusterStateProvider stateProvider = cloudManager.getClusterStateProvider(); |
| for (String c : collections) { |
| addClausesForCollection(policy, expandedClauses, stateProvider, c); |
| } |
| |
| Collections.sort(expandedClauses); |
| |
| matrix = new ArrayList<>(nodes.size()); |
| for (String node : nodes) matrix.add(new Row(node, policy.getParams(), policy.getPerReplicaAttributes(), this)); |
| applyRules(); |
| } |
| |
| /** |
| * Creates a new Session and updates the Rows in the internal matrix to reference this session. |
| */ |
| private Session(List<String> nodes, SolrCloudManager cloudManager, |
| List<Row> matrix, Set<String> collections, List<Clause> expandedClauses, |
| NodeStateProvider nodeStateProvider, Policy policy, Transaction transaction) { |
| this.transaction = transaction; |
| this.policy = policy; |
| this.nodes = nodes; |
| this.cloudManager = cloudManager; |
| this.collections = collections; |
| this.matrix = matrix; |
| this.expandedClauses = expandedClauses; |
| this.nodeStateProvider = nodeStateProvider; |
| for (Row row : matrix) row.session = this; |
| } |
| |
| /** |
| * Given a session (this one), creates a new one for placement simulations that retains all the relevant information, |
| * whether or not that info already made it to Zookeeper. |
| */ |
| public Session cloneToNewSession(SolrCloudManager cloudManager) { |
| NodeStateProvider nodeStateProvider = cloudManager.getNodeStateProvider(); |
| ClusterStateProvider clusterStateProvider = cloudManager.getClusterStateProvider(); |
| |
| List<String> nodes = new ArrayList<>(clusterStateProvider.getLiveNodes()); |
| |
| // Copy all collections from old session, even those not yet in ZK state |
| Set<String> collections = new HashSet<>(this.collections); |
| |
| // (shallow) copy the expanded clauses |
| List<Clause> expandedClauses = new ArrayList<>(this.expandedClauses); |
| |
| List<Row> matrix = new ArrayList<>(nodes.size()); |
| Map<String, Row> copyNodes = new HashMap<>(); |
| for (Row oldRow: this.matrix) { |
| copyNodes.put(oldRow.node, oldRow.copy()); |
| } |
| for (String node : nodes) { |
| // Do we have a row for that node in this session? If yes, reuse without trying to fetch from cluster state (latest changes might not be there) |
| Row newRow = copyNodes.get(node); |
| if (newRow == null) { |
| // Dealing with a node that doesn't exist in this Session. Need to create related data from scratch. |
| // We pass null for the Session in purpose. The current (this) session in not the correct one for this Row. |
| // The correct session will be set when we build the new Session instance at the end of this method. |
| newRow = new Row(node, this.policy.getParams(), this.policy.getPerReplicaAttributes(), null, nodeStateProvider, cloudManager); |
| // Get info for collections on that node |
| Set<String> collectionsOnNewNode = nodeStateProvider.getReplicaInfo(node, Collections.emptyList()).keySet(); |
| collections.addAll(collectionsOnNewNode); |
| |
| // Adjust policies to take into account new collections |
| for (String collection : collectionsOnNewNode) { |
| // We pass this.policy but it is not modified so will not impact this session being cloned |
| addClausesForCollection(this.policy, expandedClauses, clusterStateProvider, collection); |
| } |
| } |
| matrix.add(newRow); |
| } |
| |
| if (nodes.size() > 0) { |
| //if any collection has 'withCollection' irrespective of the node, the NodeStateProvider returns a map value |
| Map<String, Object> vals = nodeStateProvider.getNodeValues(nodes.get(0), Collections.singleton("withCollection")); |
| if (!vals.isEmpty() && vals.get("withCollection") != null) { |
| @SuppressWarnings({"unchecked"}) |
| Map<String, String> withCollMap = (Map<String, String>) vals.get("withCollection"); |
| if (!withCollMap.isEmpty()) { |
| @SuppressWarnings({"unchecked"}) |
| Clause withCollClause = new Clause((Map<String,Object>)Utils.fromJSONString("{withCollection:'*' , node: '#ANY'}") , |
| new Condition(NODE.tagName, "#ANY", Operand.EQUAL, null, null), |
| new Condition(WITH_COLLECTION.tagName,"*" , Operand.EQUAL, null, null), true, null, false |
| ); |
| expandedClauses.add(withCollClause); |
| } |
| } |
| } |
| |
| Collections.sort(expandedClauses); |
| |
| Session newSession = new Session(nodes, cloudManager, matrix, collections, expandedClauses, |
| nodeStateProvider, this.policy, this.transaction); |
| newSession.applyRules(); |
| |
| return newSession; |
| } |
| |
| void addClausesForCollection(ClusterStateProvider stateProvider, String collection) { |
| addClausesForCollection(policy, expandedClauses, stateProvider, collection); |
| } |
| |
| public static void addClausesForCollection(Policy policy, List<Clause> clauses, ClusterStateProvider stateProvider, String collectionName) { |
| String p = stateProvider.getPolicyNameByCollection(collectionName); |
| if (p != null) { |
| List<Clause> perCollPolicy = policy.getPolicies().get(p); |
| if (perCollPolicy == null) { |
| return; |
| } |
| } |
| clauses.addAll(mergePolicies(collectionName, policy.getPolicies().getOrDefault(p, emptyList()), policy.getClusterPolicy())); |
| } |
| |
| Session copy() { |
| return new Session(nodes, cloudManager, getMatrixCopy(), new HashSet<>(), expandedClauses, nodeStateProvider, policy, transaction); |
| } |
| |
| public Row getNode(String node) { |
| for (Row row : matrix) if (row.node.equals(node)) return row; |
| return null; |
| } |
| |
| List<Row> getMatrixCopy() { |
| return matrix.stream() |
| .map(row -> row.copy()) |
| .collect(Collectors.toList()); |
| } |
| |
| public Policy getPolicy() { |
| return policy; |
| |
| } |
| |
| /** |
| * Apply the preferences and conditions |
| */ |
| void applyRules() { |
| sortNodes(); |
| |
| for (Clause clause : expandedClauses) { |
| List<Violation> errs = clause.test(this, null); |
| violations.addAll(errs); |
| } |
| } |
| |
| void sortNodes() { |
| setApproxValuesAndSortNodes(policy.getClusterPreferences(), matrix); |
| } |
| |
| |
| public List<Violation> getViolations() { |
| return violations; |
| } |
| |
| public Suggester getSuggester(CollectionAction action) { |
| Suggester op = ops.get(action).get(); |
| if (op == null) throw new UnsupportedOperationException(action.toString() + "is not supported"); |
| op._init(this); |
| return op; |
| } |
| |
| @Override |
| public void writeMap(EntryWriter ew) throws IOException { |
| for (Row row : matrix) { |
| ew.put(row.node, row); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return Utils.toJSONString(toMap(new LinkedHashMap<>())); |
| } |
| |
| public List<Row> getSortedNodes() { |
| return Collections.unmodifiableList(matrix); |
| } |
| |
| public NodeStateProvider getNodeStateProvider() { |
| return nodeStateProvider; |
| } |
| |
| public int indexOf(String node) { |
| for (int i = 0; i < matrix.size(); i++) if (matrix.get(i).node.equals(node)) return i; |
| throw new RuntimeException("NO such node found " + node); |
| } |
| } |
| } |