/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.probe; import static com.google_voltpatches.common.base.Preconditions.checkArgument; import static com.google_voltpatches.common.base.Preconditions.checkNotNull; import static com.google_voltpatches.common.base.Predicates.equalTo; import static com.google_voltpatches.common.base.Predicates.not; import java.net.InetAddress; import java.util.Arrays; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Generated; import org.apache.zookeeper_voltpatches.KeeperException; import org.apache.zookeeper_voltpatches.ZooKeeper; import org.apache.zookeeper_voltpatches.data.Stat; import org.json_voltpatches.JSONException; import org.json_voltpatches.JSONObject; import org.json_voltpatches.JSONStringer; import org.json_voltpatches.JSONWriter; import org.voltcore.common.Constants; import org.voltcore.logging.VoltLogger; import org.voltcore.messaging.HostMessenger; import org.voltcore.messaging.JoinAcceptor; import org.voltcore.messaging.SocketJoiner; import org.voltcore.utils.VersionChecker; import org.voltcore.zk.CoreZK; import org.voltdb.StartAction; import org.voltdb.common.NodeState; import org.voltdb.utils.Digester; import org.voltdb.utils.MiscUtils; import com.google_voltpatches.common.base.Joiner; import com.google_voltpatches.common.base.Predicate; import com.google_voltpatches.common.base.Splitter; import com.google_voltpatches.common.base.Supplier; import com.google_voltpatches.common.base.Suppliers; import com.google_voltpatches.common.base.Throwables; import com.google_voltpatches.common.collect.ImmutableMap; import com.google_voltpatches.common.collect.ImmutableSortedSet; import com.google_voltpatches.common.collect.Maps; import com.google_voltpatches.common.net.HostAndPort; import com.google_voltpatches.common.net.InetAddresses; import com.google_voltpatches.common.net.InternetDomainName; import com.google_voltpatches.common.util.concurrent.SettableFuture; /** * The VoltDB implementation of {@link JoinAcceptor} that piggy backs the mesh * establishment messages to determine if connecting nodes are compatible, and * if they are, it determines the voltdb start action from the information * gathered from other connecting nodes. * */ public class MeshProber implements JoinAcceptor { private static final String COORDINATORS = "coordinators"; private static final String SAFE_MODE = "safeMode"; private static final String ADD_ALLOWED = "addAllowed"; private static final String PAUSED = "paused"; private static final String K_FACTOR = "kFactor"; private static final String HOST_COUNT = "hostCount"; private static final String MESH_HASH = "meshHash"; private static final String CONFIG_HASH = "configHash"; private static final String BARE = "bare"; private static final String START_ACTION = "startAction"; private static final String ENTERPRISE = "enterprise"; private static final String TERMINUS_NONCE = "terminusNonce"; private static final String MISSING_HOST_COUNT = "missingHostCount"; private static final VoltLogger m_networkLog = new VoltLogger("NETWORK"); /** * Helper method that takes a comma delimited list of host specs, validates it, * and converts it to a set of valid coordinators * @param option a string that contains comma delimited list of host specs * @return a set of valid coordinators */ public static ImmutableSortedSet<String> hosts(String option) { checkArgument(option != null, "option is null"); if (option.trim().isEmpty()) { return ImmutableSortedSet.of( HostAndPort.fromParts("", Constants.DEFAULT_INTERNAL_PORT).toString()); } Splitter commaSplitter = Splitter.on(',').omitEmptyStrings().trimResults(); ImmutableSortedSet.Builder<String> sbld = ImmutableSortedSet.naturalOrder(); for (String h: commaSplitter.split(option)) { checkArgument(isValidCoordinatorSpec(h), "%s is not a valid host spec", h); sbld.add(HostAndPort.fromString(h).withDefaultPort(Constants.DEFAULT_INTERNAL_PORT).toString()); } return sbld.build(); } /** * Convenience method mainly used in local cluster testing * * @param ports a list of ports * @return a set of coordinator specs */ public static ImmutableSortedSet<String> hosts(int...ports) { if (ports.length == 0) { return ImmutableSortedSet.of( HostAndPort.fromParts("", Constants.DEFAULT_INTERNAL_PORT).toString()); } ImmutableSortedSet.Builder<String> sbld = ImmutableSortedSet.naturalOrder(); for (int p: ports) { sbld.add(HostAndPort.fromParts("", p).toString()); } return sbld.build(); } public static boolean isValidCoordinatorSpec(String specifier) { if (specifier == null) { return false; } if (specifier.isEmpty()) { return true; } final HostAndPort parsedHost = HostAndPort .fromString(specifier) .withDefaultPort(Constants.DEFAULT_INTERNAL_PORT); final String host = parsedHost.getHostText(); if (host.isEmpty()) { return true; } // Try to interpret the specifier as an IP address. Note we build // the address rather than using the .is* methods because we want to // use InetAddresses.toUriString to convert the result to a string in // canonical form. InetAddress addr = null; try { addr = InetAddresses.forString(host); } catch (IllegalArgumentException e) { // It is not an IPv4 or IPv6 literal } if (addr != null) { return true; } // It is not any kind of IP address; must be a domain name or invalid. return InternetDomainName.isValid(host); } public static final Predicate<Integer> in(final Set<Integer> set) { return new Predicate<Integer>() { @Override public boolean apply(Integer input) { return set.contains(input); } }; } public static MeshProber prober(HostMessenger hm) { return MeshProber.class.cast(hm.getAcceptor()); } protected final ImmutableSortedSet<String> m_coordinators; protected final VersionChecker m_versionChecker; protected final boolean m_enterprise; protected final StartAction m_startAction; /** {@code true} if there are no recoverable artifacts (Command Logs, Snapshots) */ protected final boolean m_bare; protected final UUID m_configHash; protected final UUID m_meshHash; protected final Supplier<Integer> m_hostCountSupplier; protected final int m_kFactor; protected final boolean m_paused; protected final Supplier<NodeState> m_nodeStateSupplier; protected final boolean m_addAllowed; protected final boolean m_safeMode; protected final String m_terminusNonce; protected final int m_missingHostCount; protected final HostCriteriaRef m_hostCriteria = new HostCriteriaRef(); /* * on probe startup mode this future is set when there are enough * hosts to matched the configured cluster size */ private final SettableFuture<Determination> m_probedDetermination = SettableFuture.create(); protected MeshProber(NavigableSet<String> coordinators, VersionChecker versionChecker, boolean enterprise, StartAction startAction, boolean bare, UUID configHash, Supplier<Integer> hostCountSupplier, int kFactor, boolean paused, Supplier<NodeState> nodeStateSupplier, boolean addAllowed, boolean safeMode, String terminusNonce, int missingHostCount) { checkArgument(versionChecker != null, "version checker is null"); checkArgument(configHash != null, "config hash is null"); checkArgument(startAction != null, "start action is null"); checkArgument(nodeStateSupplier != null, "nodeStateSupplier is null"); checkArgument(hostCountSupplier != null, "hostCountSupplier is null"); checkArgument(kFactor >= 0, "invalid kFactor value: %s", kFactor); checkArgument(coordinators != null && coordinators.stream().allMatch(h->isValidCoordinatorSpec(h)), "coordinators is null or contains invalid host/interface specs %s", coordinators); checkArgument(coordinators.size() <= hostCountSupplier.get(), "host count %s is less then the number of coordinators %s", hostCountSupplier.get(), coordinators.size()); checkArgument(terminusNonce == null || !terminusNonce.trim().isEmpty(), "terminus should not be blank"); this.m_coordinators = ImmutableSortedSet.copyOf(coordinators); this.m_versionChecker = versionChecker; this.m_enterprise = enterprise; this.m_startAction = startAction; this.m_bare = bare; this.m_configHash = configHash; this.m_hostCountSupplier = hostCountSupplier; this.m_kFactor = kFactor; this.m_paused = paused; this.m_nodeStateSupplier = nodeStateSupplier; this.m_addAllowed = addAllowed; this.m_safeMode = safeMode; this.m_terminusNonce = terminusNonce; this.m_missingHostCount = missingHostCount; this.m_meshHash = Digester.md5AsUUID("hostCount="+ hostCountSupplier.get() + '|' + this.m_coordinators.toString()); } public UUID getMeshHash() { return m_meshHash; } @Override public NavigableSet<String> getCoordinators() { return m_coordinators; } public NodeState getNodeState() { return m_nodeStateSupplier.get(); } @Override public HostAndPort getLeader() { return HostAndPort.fromString(m_coordinators.first()).withDefaultPort(Constants.DEFAULT_INTERNAL_PORT); } @Override public VersionChecker getVersionChecker() { return m_versionChecker; } public boolean isEnterprise() { return m_enterprise; } public StartAction getStartAction() { return m_startAction; } /** * @return {@code true} if there are no recoverable artifacts (Command Logs, Snapshots) */ public boolean isBare() { return m_bare; } public UUID getConfigHash() { return m_configHash; } public int getHostCount() { return m_hostCountSupplier.get(); } public int getkFactor() { return m_kFactor; } public boolean isPaused() { return m_paused; } public boolean isAddAllowed() { return m_addAllowed; } public boolean isSafeMode() { return m_safeMode; } public String getTerminusNonce() { return m_terminusNonce; } public int getmissingHostCount() { return m_missingHostCount; } public HostCriteria asHostCriteria() { return new HostCriteria( m_paused, m_configHash, m_meshHash, m_enterprise, m_startAction, m_bare, m_hostCountSupplier.get(), m_nodeStateSupplier.get(), m_addAllowed, m_safeMode, m_terminusNonce ); } public HostCriteria asHostCriteria(boolean paused) { return new HostCriteria( paused, m_configHash, m_meshHash, m_enterprise, m_startAction, m_bare, m_hostCountSupplier.get(), m_nodeStateSupplier.get(), m_addAllowed, m_safeMode, m_terminusNonce ); } @Override public void detract(int hostId) { checkArgument(hostId >= 0, "host id %s is not greater or equal to 0", hostId); Map<Integer,HostCriteria> expect; Map<Integer,HostCriteria> update; do { expect = m_hostCriteria.get(); update = ImmutableMap.<Integer, HostCriteria>builder() .putAll(Maps.filterKeys(expect, not(equalTo(hostId)))) .build(); } while (!m_hostCriteria.compareAndSet(expect, update)); } @Override public void detract(Set<Integer> hostIds) { checkArgument(hostIds != null, "set of host ids is null"); Map<Integer,HostCriteria> expect; Map<Integer,HostCriteria> update; do { expect = m_hostCriteria.get(); update = ImmutableMap.<Integer, HostCriteria>builder() .putAll(Maps.filterKeys(expect, not(in(hostIds)))) .build(); } while (!m_hostCriteria.compareAndSet(expect, update)); } @Override public JSONObject decorate(JSONObject jo, Optional<Boolean> paused) { return paused.map(p -> asHostCriteria(p).appendTo(jo)).orElse(asHostCriteria().appendTo(jo)); } @Override public JoinAcceptor.PleaDecision considerMeshPlea(ZooKeeper zk, int hostId, JSONObject jo) { checkArgument(zk != null, "zookeeper is null"); checkArgument(jo != null, "json object is null"); if (!HostCriteria.hasCriteria(jo)) { return new JoinAcceptor.PleaDecision( String.format("Joining node version %s is incompatible with this node verion %s", jo.optString(SocketJoiner.VERSION_STRING,"(unknown)"), m_versionChecker.getVersionString()), false, false); } HostCriteria hc = new HostCriteria(jo); Map<Integer,HostCriteria> hostCriteria = m_hostCriteria.get(); // host criteria must be strictly compatible only if no node is operational (i.e. // when the cluster is forming anew) if ( !getNodeState().operational() && !hostCriteria.values().stream().anyMatch(c->c.getNodeState().operational())) { List<String> incompatibilities = asHostCriteria().listIncompatibilities(hc); if (!incompatibilities.isEmpty()) { Joiner joiner = Joiner.on("\n ").skipNulls(); String error = "Incompatible joining criteria:\n " + joiner.join(incompatibilities); return new JoinAcceptor.PleaDecision(error, false, false); } return new JoinAcceptor.PleaDecision(null, true, false); } else { StartAction operationalStartAction = hostCriteria.values().stream() .filter(c->c.getNodeState().operational()) .map(c->c.getStartAction()) .findFirst().orElse(getStartAction()); if (operationalStartAction == StartAction.PROBE && hc.getStartAction() != StartAction.PROBE) { String msg = "Invalid VoltDB command. Please use init and start to join this cluster"; return new JoinAcceptor.PleaDecision(msg, false, false); } } // how many hosts are already in the mesh? Stat stat = new Stat(); try { zk.getChildren(CoreZK.hosts, false, stat); } catch (InterruptedException e) { String msg = "Interrupted while considering mesh plea"; m_networkLog.error(msg, e); return new JoinAcceptor.PleaDecision(msg, false, false); } catch (KeeperException e) { EnumSet<KeeperException.Code> closing = EnumSet.of( KeeperException.Code.SESSIONEXPIRED, KeeperException.Code.CONNECTIONLOSS); if (closing.contains(e.code())) { return new JoinAcceptor.PleaDecision("Shutting down", false, false); } else { String msg = "Failed to list hosts while considering a mesh plea"; m_networkLog.error(msg, e); return new JoinAcceptor.PleaDecision(msg, false, false); } } // connecting to already wholly formed cluster if (stat.getNumChildren() >= getHostCount()) { return new JoinAcceptor.PleaDecision( hc.isAddAllowed()? null : "Cluster is already complete", hc.isAddAllowed(), false); } else if (stat.getNumChildren() < getHostCount()) { // check for concurrent rejoins final int rejoiningHost = CoreZK.createRejoinNodeIndicator(zk, hostId); if (rejoiningHost == -1) { return new JoinAcceptor.PleaDecision(null, true, false); } else { String msg = "Only one host can rejoin at a time. Host " + rejoiningHost + " is still rejoining."; return new JoinAcceptor.PleaDecision(msg, false, true); } } return new JoinAcceptor.PleaDecision(null, true, false); } @Override public void accrue(int hostId, JSONObject jo) { checkArgument(hostId >= 0, "host id %s is not greater or equal to 0", hostId); checkArgument(jo != null, "json object is null"); HostCriteria hc = new HostCriteria(jo); checkArgument(!hc.isUndefined(), "json object does not contain host prober fields"); Map<Integer,HostCriteria> expect; Map<Integer,HostCriteria> update; do { expect = m_hostCriteria.get(); update = ImmutableMap.<Integer, HostCriteria>builder() .putAll(Maps.filterKeys(expect, not(equalTo(hostId)))) .put(hostId, hc) .build(); } while (!m_hostCriteria.compareAndSet(expect, update)); determineStartActionIfNecessary(update); } @Override public void accrue(Map<Integer, JSONObject> jos) { checkArgument(jos != null, "map of host ids and json object is null"); ImmutableMap.Builder<Integer, HostCriteria> hcb = ImmutableMap.builder(); for (Map.Entry<Integer, JSONObject> e: jos.entrySet()) { HostCriteria hc = new HostCriteria(e.getValue()); checkArgument(!hc.isUndefined(), "json boject for host id %s does not contain prober fields", e.getKey()); hcb.put(e.getKey(), hc); } Map<Integer, HostCriteria> additions = hcb.build(); Map<Integer,HostCriteria> expect; Map<Integer,HostCriteria> update; do { expect = m_hostCriteria.get(); update = ImmutableMap.<Integer, HostCriteria>builder() .putAll(Maps.filterKeys(expect, not(in(additions.keySet())))) .putAll(additions) .build(); } while (!m_hostCriteria.compareAndSet(expect, update)); determineStartActionIfNecessary(update); } /** * Check to see if we have enough {@link HostCriteria} gathered to make a * start action {@link Determination} */ private void determineStartActionIfNecessary(Map<Integer, HostCriteria> hostCriteria) { // already made a determination if (m_probedDetermination.isDone()) { return; } final int ksafety = getkFactor() + 1; int bare = 0; // node has no recoverable artifacts (Command Logs, Snapshots) int unmeshed = 0; int operational = 0; int haveTerminus = 0; int hostCount = getHostCount(); int missingHostCount = getmissingHostCount(); // both paused and safemode need to be specified on only one node to // make them a cluster attribute. These are overridden if there are // any nodes in operational state boolean paused = isPaused(); boolean safemode = isSafeMode(); final NavigableSet<String> terminusNonces = new TreeSet<>(); for (HostCriteria c: hostCriteria.values()) { if (c.getNodeState().operational()) { operational += 1; // pause state from operational nodes overrides yours // prefer host count from operational nodes if (operational == 1) { paused = c.isPaused(); hostCount = c.getHostCount(); } } unmeshed += c.getNodeState().unmeshed() ? 1 : 0; bare += c.isBare() ? 1 : 0; if (c.isPaused() && operational == 0) { paused = c.isPaused(); } safemode = safemode || c.isSafeMode(); if (c.getTerminusNonce() != null) { terminusNonces.add(c.getTerminusNonce()); ++haveTerminus; } } int expectedHostCount = hostCount - missingHostCount; // not enough host criteria to make a determination if (hostCriteria.size() < expectedHostCount && operational == 0) { m_networkLog.debug("have yet to receive all the required host criteria"); return; } // handle add (i.e. join) cases too if (hostCount < getHostCount() && hostCriteria.size() <= expectedHostCount) { m_networkLog.debug("have yet to receive all the required host criteria"); return; } m_networkLog.debug("Received all the required host criteria"); safemode = safemode && operational == 0 && bare < ksafety; // kfactor + 1 if (m_networkLog.isDebugEnabled()) { m_networkLog.debug("We have " + operational + " operational nodes, " + bare + " bare nodes, and " + unmeshed + " unmeshed nodes"); m_networkLog.debug("Propagated cluster attribute are paused: " + paused + ", and safemode: " + safemode); } if (terminusNonces.size() > 1) { org.voltdb.VoltDB.crashLocalVoltDB("Detected multiple startup snapshots, cannot " + "proceed with cluster startup. Snapshot IDs " + terminusNonces); } String terminusNonce = terminusNonces.pollFirst(); if (operational == 0 && haveTerminus <= (hostCount - ksafety)) { terminusNonce = null; } if (getStartAction() != StartAction.PROBE) { m_probedDetermination.set(new Determination( getStartAction(), getHostCount(), paused, terminusNonce)); return; } StartAction determination = isBare() ? StartAction.CREATE : StartAction.RECOVER; if (operational > 0 && operational < hostCount) { // rejoin determination = StartAction.LIVE_REJOIN; } else if (operational > 0 && operational == hostCount) { // join if (isAddAllowed()) { hostCount = hostCount + ksafety; // kfactor + 1 determination = StartAction.JOIN; } else { org.voltdb.VoltDB.crashLocalVoltDB("Node is not allowed to rejoin an already complete cluster"); return; } } else if (operational == 0 && bare == unmeshed) { determination = StartAction.CREATE; } else if (operational == 0 && bare < ksafety /* kfactor + 1 */) { determination = safemode ? StartAction.SAFE_RECOVER : StartAction.RECOVER; } else if (operational == 0 && bare >= ksafety /* kfactor + 1 */) { org.voltdb.VoltDB.crashLocalVoltDB("Cluster has incomplete command logs: " + bare + " nodes have no command logs, while " + (unmeshed - bare) + " nodes have them"); return; } final Determination dtrm = new Determination(determination, hostCount, paused, terminusNonce); if (m_networkLog.isDebugEnabled()) { m_networkLog.debug("made the following " + dtrm); } m_probedDetermination.set(dtrm); } public Determination waitForDetermination() { try { return m_probedDetermination.get(); } catch (ExecutionException notThrownBecauseItIsASettableFuture) { } catch (InterruptedException e) { org.voltdb.VoltDB.crashLocalVoltDB( "interrupted while waiting to determine the cluster start action", false, e); } return new Determination(null,-1, false, null); } @Generated("by eclipse's equals and hashCode source generators") @Override public String toString() { return "MeshProber [coordinators=" + m_coordinators + ", enterprise=" + m_enterprise + ", startAction=" + m_startAction + ", bare=" + m_bare + ", configHash=" + m_configHash + ", meshHash=" + m_meshHash + ", hostCount=" + m_hostCountSupplier.get() + ", kFactor=" + m_kFactor + ", paused=" + m_paused + ", addAllowed=" + m_addAllowed + ", safeMode=" + m_safeMode + ", missingHostCount=" + m_missingHostCount + "]"; } public void appendTo(JSONWriter jw) throws JSONException { jw.object(); jw.key(COORDINATORS).array(); for (String coordinator: m_coordinators) { jw.value(coordinator); } jw.endArray(); jw.keySymbolValuePair(ENTERPRISE, m_enterprise); jw.keySymbolValuePair(START_ACTION, m_startAction.name()); jw.keySymbolValuePair(BARE, m_bare); jw.keySymbolValuePair(CONFIG_HASH, m_configHash.toString()); jw.keySymbolValuePair(MESH_HASH, m_meshHash.toString()); jw.keySymbolValuePair(HOST_COUNT, m_hostCountSupplier.get()); jw.keySymbolValuePair(K_FACTOR, m_kFactor); jw.keySymbolValuePair(PAUSED, m_paused); jw.keySymbolValuePair(ADD_ALLOWED, m_addAllowed); jw.keySymbolValuePair(SAFE_MODE, m_safeMode); jw.keySymbolValuePair(TERMINUS_NONCE, m_terminusNonce); jw.keySymbolValuePair(MISSING_HOST_COUNT, m_missingHostCount); jw.endObject(); } @Override public String toJSONString() { JSONStringer js = new JSONStringer(); try { appendTo(js); } catch (JSONException e) { Throwables.propagate(e); } return js.toString(); } public static class Determination { public final StartAction startAction; public final int hostCount; public final boolean paused; public final String terminusNonce; private Determination(StartAction startAction, int hostCount, boolean paused, String terminusNonce) { this.startAction = startAction; this.hostCount = hostCount; this.paused = paused; this.terminusNonce = terminusNonce; } @Override @Generated("by eclipse's equals and hashCode source generators") public int hashCode() { final int prime = 31; int result = 1; result = prime * result + hostCount; result = prime * result + (paused ? 1231 : 1237); result = prime * result + ((startAction == null) ? 0 : startAction.hashCode()); result = prime * result + ((terminusNonce == null) ? 0 : terminusNonce.hashCode()); return result; } @Override @Generated("by eclipse's equals and hashCode source generators") public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Determination other = (Determination) obj; if (hostCount != other.hostCount) return false; if (paused != other.paused) return false; if (startAction != other.startAction) return false; if (terminusNonce == null) { if (other.terminusNonce != null) return false; } else if (!terminusNonce.equals(other.terminusNonce)) return false; return true; } @Override public String toString() { return "Determination [startAction=" + startAction + ", hostCount=" + hostCount + ", paused=" + paused + ", terminusNonce=" + terminusNonce + "]"; } } public static Builder builder() { return new Builder(); } public static class Builder { protected final VersionChecker m_defaultVersionChecker = JoinAcceptor.DEFAULT_VERSION_CHECKER; protected NavigableSet<String> m_coordinators = ImmutableSortedSet.of("localhost"); protected VersionChecker m_versionChecker = m_defaultVersionChecker; protected boolean m_enterprise = MiscUtils.isPro(); protected StartAction m_startAction = StartAction.PROBE; protected boolean m_bare = true; protected UUID m_configHash = new UUID(0L, 0L); protected Supplier<Integer> m_hostCountSupplier; protected int m_kFactor = 0; protected boolean m_paused = false; protected Supplier<NodeState> m_nodeStateSupplier = Suppliers.ofInstance(NodeState.INITIALIZING); protected boolean m_addAllowed = false; protected boolean m_safeMode = false; protected String m_terminusNonce = null; protected int m_missingHostCount = 0; protected Builder() { } public Builder prober(MeshProber o) { m_coordinators = ImmutableSortedSet.copyOf(checkNotNull(o).m_coordinators); m_versionChecker = o.m_versionChecker; m_enterprise = o.m_enterprise; m_startAction = o.m_startAction; m_bare = o.m_bare; m_configHash = o.m_configHash; m_hostCountSupplier = o.m_hostCountSupplier; m_kFactor = o.m_kFactor; m_paused = o.m_paused; m_nodeStateSupplier = o.m_nodeStateSupplier; m_addAllowed = o.m_addAllowed; m_safeMode = o.m_safeMode; m_terminusNonce = o.m_terminusNonce; m_missingHostCount = o.m_missingHostCount; return this; } public Builder versionChecker(VersionChecker versionChecker) { m_versionChecker = checkNotNull(versionChecker); return this; } public Builder startAction(StartAction startAction) { m_startAction = checkNotNull(startAction); return this; } public Builder nodeState(NodeState nodeState) { m_nodeStateSupplier = Suppliers.ofInstance(checkNotNull(nodeState)); return this; } public Builder configHash(UUID configHash) { m_configHash = checkNotNull(configHash); return this; } public Builder coordinators(NavigableSet<String> coordinators) { m_coordinators = checkNotNull(coordinators); return this; } public Builder coordinators(String...hosts) { checkArgument(hosts.length > 0, "no hosts provided"); checkArgument(Arrays.stream(hosts).allMatch(h->isValidCoordinatorSpec(h)), "coordinators contains invalid host/interface specs %s", Arrays.toString(hosts)); m_coordinators = ImmutableSortedSet.copyOf(hosts); return this; } public Builder bare(boolean bare) { m_bare = bare; return this; } public Builder enterprise(boolean enterprise) { m_enterprise = enterprise; return this; } public Builder paused(boolean paused) { m_paused = paused; return this; } public Builder kfactor(int kfactor) { m_kFactor = kfactor; return this; } public Builder hostCount(int hostCount) { m_hostCountSupplier = Suppliers.ofInstance(hostCount); return this; } public Builder hostCountSupplier(Supplier<Integer> supplier) { m_hostCountSupplier = supplier; return this; } public Builder nodeStateSupplier(Supplier<NodeState> supplier) { m_nodeStateSupplier = supplier; return this; } public Builder addAllowed(boolean addAllowed) { m_addAllowed = addAllowed; return this; } public Builder safeMode(boolean safeMode) { m_safeMode = safeMode; return this; } public Builder terminusNonce(String terminusNonce) { m_terminusNonce = terminusNonce; return this; } public Builder missingHostCount(int missingHostCount) { m_missingHostCount = missingHostCount; return this; } public MeshProber build() { if (m_hostCountSupplier == null && m_coordinators != null) { m_hostCountSupplier = Suppliers.ofInstance(m_coordinators.size()); } return new MeshProber( m_coordinators, m_versionChecker, m_enterprise, m_startAction, m_bare, m_configHash, m_hostCountSupplier, m_kFactor, m_paused, m_nodeStateSupplier, m_addAllowed, m_safeMode, m_terminusNonce, m_missingHostCount ); } } final static class HostCriteriaRef extends AtomicReference<Map<Integer, HostCriteria>> { private static final long serialVersionUID = -7947013480687680553L; final static Map<Integer,HostCriteria> EMPTY_MAP = ImmutableMap.of(); public HostCriteriaRef(Map<Integer, HostCriteria> map) { super(map); } public HostCriteriaRef() { this(EMPTY_MAP); } } }