/* * 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.brooklyn.entity.nosql.cassandra; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.policy.PolicySpec; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.SensorEvent; import org.apache.brooklyn.api.sensor.SensorEventListener; import org.apache.brooklyn.core.effector.EffectorBody; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityPredicates; import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic; import org.apache.brooklyn.core.location.Machines; import org.apache.brooklyn.enricher.stock.Enrichers; import org.apache.brooklyn.entity.group.AbstractMembershipTrackingPolicy; import org.apache.brooklyn.entity.group.DynamicClusterImpl; import org.apache.brooklyn.entity.group.DynamicGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.time.Time; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.net.HostAndPort; /** * Implementation of {@link CassandraDatacenter}. * <p> * Several subtleties to note: * - a node may take some time after it is running and serving JMX to actually be contactable on its thrift port * (so we wait for thrift port to be contactable) * - sometimes new nodes take a while to peer, and/or take a while to get a consistent schema * (each up to 1m; often very close to the 1m) */ public class CassandraDatacenterImpl extends DynamicClusterImpl implements CassandraDatacenter { /* * TODO Seed management is hard! * - The ServiceRestarter is not doing customize(), so is not refreshing the seeds in cassandra.yaml. * If we have two nodes that were seeds for each other and they both restart at the same time, we'll have a split brain. */ private static final Logger log = LoggerFactory.getLogger(CassandraDatacenterImpl.class); // Mutex for synchronizing during re-size operations private final Object mutex = new Object[0]; private final Supplier<Set<Entity>> defaultSeedSupplier = new Supplier<Set<Entity>>() { // Mutex for (re)calculating our seeds // TODO is this very dangerous?! Calling out to SeedTracker, which calls out to alien getAttribute()/getConfig(). But I think that's ok. // TODO might not need mutex? previous race was being caused by something else, other than concurrent calls! private final Object seedMutex = new Object(); @Override public Set<Entity> get() { synchronized (seedMutex) { boolean hasPublishedSeeds = Boolean.TRUE.equals(getAttribute(HAS_PUBLISHED_SEEDS)); int quorumSize = getSeedQuorumSize(); Set<Entity> potentialSeeds = gatherPotentialSeeds(); Set<Entity> potentialRunningSeeds = gatherPotentialRunningSeeds(); boolean stillWaitingForQuorum = (!hasPublishedSeeds) && (potentialSeeds.size() < quorumSize); if (stillWaitingForQuorum) { if (log.isDebugEnabled()) log.debug("Not refreshed seeds of cluster {}, because still waiting for quorum (need {}; have {} potentials)", new Object[] {CassandraDatacenterImpl.class, quorumSize, potentialSeeds.size()}); return ImmutableSet.of(); } else if (hasPublishedSeeds) { Set<Entity> currentSeeds = getAttribute(CURRENT_SEEDS); if (getAttribute(SERVICE_STATE_ACTUAL) == Lifecycle.STARTING) { if (Sets.intersection(currentSeeds, potentialSeeds).isEmpty()) { log.warn("Cluster {} lost all its seeds while starting! Subsequent failure likely, but changing seeds during startup would risk split-brain: seeds={}", new Object[] {CassandraDatacenterImpl.this, currentSeeds}); } return currentSeeds; } else if (potentialRunningSeeds.isEmpty()) { // TODO Could be race where nodes have only just returned from start() and are about to // transition to serviceUp; so don't just abandon all our seeds! log.warn("Cluster {} has no running seeds (yet?); leaving seeds as-is; but risks split-brain if these seeds come back up!", new Object[] {CassandraDatacenterImpl.this}); return currentSeeds; } else { Set<Entity> result = trim(quorumSize, potentialRunningSeeds); log.debug("Cluster {} updating seeds: chosen={}; potentialRunning={}", new Object[] {CassandraDatacenterImpl.this, result, potentialRunningSeeds}); return result; } } else { Set<Entity> result = trim(quorumSize, potentialSeeds); if (log.isDebugEnabled()) log.debug("Cluster {} has reached seed quorum: seeds={}", new Object[] {CassandraDatacenterImpl.this, result}); return result; } } } private Set<Entity> trim(int num, Set<Entity> contenders) { // Prefer existing seeds wherever possible; otherwise accept any other contenders Set<Entity> currentSeeds = (getAttribute(CURRENT_SEEDS) != null) ? getAttribute(CURRENT_SEEDS) : ImmutableSet.<Entity>of(); Set<Entity> result = Sets.newLinkedHashSet(); result.addAll(Sets.intersection(currentSeeds, contenders)); result.addAll(contenders); return ImmutableSet.copyOf(Iterables.limit(result, num)); } }; protected SeedTracker seedTracker = new SeedTracker(); protected TokenGenerator tokenGenerator = null; public CassandraDatacenterImpl() { } @Override public void init() { super.init(); /* * subscribe to hostname, and keep an accurate set of current seeds in a sensor; * then at nodes we set the initial seeds to be the current seeds when ready (non-empty) */ subscriptions().subscribeToMembers(this, Attributes.HOSTNAME, new SensorEventListener<String>() { @Override public void onEvent(SensorEvent<String> event) { seedTracker.onHostnameChanged(event.getSource(), event.getValue()); } }); subscriptions().subscribe(this, DynamicGroup.MEMBER_REMOVED, new SensorEventListener<Entity>() { @Override public void onEvent(SensorEvent<Entity> event) { seedTracker.onMemberRemoved(event.getValue()); } }); subscriptions().subscribeToMembers(this, Attributes.SERVICE_UP, new SensorEventListener<Boolean>() { @Override public void onEvent(SensorEvent<Boolean> event) { seedTracker.onServiceUpChanged(event.getSource(), event.getValue()); } }); subscriptions().subscribeToMembers(this, Attributes.SERVICE_STATE_ACTUAL, new SensorEventListener<Lifecycle>() { @Override public void onEvent(SensorEvent<Lifecycle> event) { // trigger a recomputation also when lifecycle state changes, // because it might not have ruled a seed as inviable when service up went true // because service state was not yet running seedTracker.onServiceUpChanged(event.getSource(), Lifecycle.RUNNING==event.getValue()); } }); // Track the datacenters for this cluster subscriptions().subscribeToMembers(this, CassandraNode.DATACENTER_NAME, new SensorEventListener<String>() { @Override public void onEvent(SensorEvent<String> event) { Entity member = event.getSource(); String dcName = event.getValue(); if (dcName != null) { Multimap<String, Entity> datacenterUsage = getAttribute(DATACENTER_USAGE); Multimap<String, Entity> mutableDatacenterUsage = (datacenterUsage == null) ? LinkedHashMultimap.<String, Entity>create() : LinkedHashMultimap.create(datacenterUsage); Optional<String> oldDcName = getKeyOfVal(mutableDatacenterUsage, member); if (!(oldDcName.isPresent() && dcName.equals(oldDcName.get()))) { mutableDatacenterUsage.values().remove(member); mutableDatacenterUsage.put(dcName, member); sensors().set(DATACENTER_USAGE, mutableDatacenterUsage); sensors().set(DATACENTERS, Sets.newLinkedHashSet(mutableDatacenterUsage.keySet())); } } } private <K,V> Optional<K> getKeyOfVal(Multimap<K,V> map, V val) { for (Map.Entry<K,V> entry : map.entries()) { if (Objects.equal(val, entry.getValue())) { return Optional.of(entry.getKey()); } } return Optional.absent(); } }); subscriptions().subscribe(this, DynamicGroup.MEMBER_REMOVED, new SensorEventListener<Entity>() { @Override public void onEvent(SensorEvent<Entity> event) { Entity entity = event.getSource(); Multimap<String, Entity> datacenterUsage = getAttribute(DATACENTER_USAGE); if (datacenterUsage != null && datacenterUsage.containsValue(entity)) { Multimap<String, Entity> mutableDatacenterUsage = LinkedHashMultimap.create(datacenterUsage); mutableDatacenterUsage.values().remove(entity); sensors().set(DATACENTER_USAGE, mutableDatacenterUsage); sensors().set(DATACENTERS, Sets.newLinkedHashSet(mutableDatacenterUsage.keySet())); } } }); getMutableEntityType().addEffector(EXECUTE_SCRIPT, new EffectorBody<String>() { @Override public String call(ConfigBag parameters) { return executeScript((String)parameters.getStringKey("commands")); } }); } protected Supplier<Set<Entity>> getSeedSupplier() { Supplier<Set<Entity>> seedSupplier = getConfig(SEED_SUPPLIER); return (seedSupplier == null) ? defaultSeedSupplier : seedSupplier; } protected boolean useVnodes() { return Boolean.TRUE.equals(getConfig(USE_VNODES)); } protected synchronized TokenGenerator getTokenGenerator() { if (tokenGenerator!=null) return tokenGenerator; try { tokenGenerator = getConfig(TOKEN_GENERATOR_CLASS).newInstance(); BigInteger shift = getConfig(TOKEN_SHIFT); if (shift==null) shift = BigDecimal.valueOf(Math.random()).multiply( new BigDecimal(tokenGenerator.range())).toBigInteger(); tokenGenerator.setOrigin(shift); return tokenGenerator; } catch (Exception e) { throw Throwables.propagate(e); } } protected int getSeedQuorumSize() { Integer quorumSize = getConfig(INITIAL_QUORUM_SIZE); if (quorumSize!=null && quorumSize>0) return quorumSize; // default 2 is recommended, unless initial size is smaller return Math.min(Math.max(getConfig(INITIAL_SIZE), 1), DEFAULT_SEED_QUORUM); } @Override public Set<Entity> gatherPotentialSeeds() { return seedTracker.gatherPotentialSeeds(); } @Override public Set<Entity> gatherPotentialRunningSeeds() { return seedTracker.gatherPotentialRunningSeeds(); } /** * Sets the default {@link #MEMBER_SPEC} to describe the Cassandra nodes. */ @Override protected EntitySpec<?> getMemberSpec() { return getConfig(MEMBER_SPEC, EntitySpec.create(CassandraNode.class)); } @Override public String getClusterName() { return getAttribute(CLUSTER_NAME); } @Override public Collection<Entity> grow(int delta) { if (useVnodes()) { // nothing to do for token generator } else { if (getCurrentSize() == 0) { getTokenGenerator().growingCluster(delta); } } return super.grow(delta); } @Override protected Entity createNode(@Nullable Location loc, Map<?,?> flags) { Map<Object, Object> allflags = MutableMap.copyOf(flags); if (flags.containsKey("token") || flags.containsKey("cassandra.token")) { // TODO Delete in future version; was deprecated in 0.7.0; deleted config key in 0.9.0 log.warn("Cassandra token no longer supported - use 'tokens' in "+CassandraDatacenterImpl.this); } if (flags.containsKey(CassandraNode.TOKENS) || flags.containsKey("tokens") || flags.containsKey("cassandra.tokens")) { // leave token config as-is } else if (!useVnodes()) { BigInteger token = getTokenGenerator().newToken(); if (token != null) { allflags.put(CassandraNode.TOKENS, ImmutableSet.of(token)); } } if ((flags.containsKey(CassandraNode.NUM_TOKENS_PER_NODE) || flags.containsKey("numTokensPerNode"))) { // leave num_tokens as-is } else if (useVnodes()) { Integer numTokensPerNode = getConfig(NUM_TOKENS_PER_NODE); allflags.put(CassandraNode.NUM_TOKENS_PER_NODE, numTokensPerNode); } else { allflags.put(CassandraNode.NUM_TOKENS_PER_NODE, 1); } return super.createNode(loc, allflags); } @Override protected Entity replaceMember(Entity member, Location memberLoc, Map<?, ?> extraFlags) { Set<BigInteger> oldTokens = ((CassandraNode) member).getTokens(); Set<BigInteger> newTokens = (oldTokens != null && oldTokens.size() > 0) ? getTokenGenerator().getTokensForReplacementNode(oldTokens) : null; return super.replaceMember(member, memberLoc, MutableMap.copyOf(extraFlags).add(CassandraNode.TOKENS, newTokens)); } @Override public void start(Collection<? extends Location> locations) { Machines.warnIfLocalhost(locations, "CassandraCluster does not support multiple nodes on localhost, " + "due to assumptions Cassandra makes about the use of the same port numbers used across the cluster."); // force this to be set - even if it is using the default sensors().set(CLUSTER_NAME, getConfig(CLUSTER_NAME)); super.start(locations); connectSensors(); // TODO wait until all nodes which we think are up are consistent // i.e. all known nodes use the same schema, as reported by // SshEffectorTasks.ssh("echo \"describe cluster;\" | /bin/cassandra-cli"); // once we've done that we can revert to using 2 seed nodes. // see CassandraCluster.DEFAULT_SEED_QUORUM // (also ensure the cluster is ready if we are about to run a creation script) Time.sleep(getConfig(DELAY_BEFORE_ADVERTISING_CLUSTER)); String scriptUrl = getConfig(CassandraNode.CREATION_SCRIPT_URL); if (Strings.isNonEmpty(scriptUrl)) { executeScript(new ResourceUtils(this).getResourceAsString(scriptUrl)); } update(); } protected void connectSensors() { connectEnrichers(); policies().add(PolicySpec.create(MemberTrackingPolicy.class) .displayName("Cassandra Cluster Tracker") .configure("sensorsToTrack", ImmutableSet.of(Attributes.SERVICE_UP, Attributes.HOSTNAME, CassandraNode.THRIFT_PORT)) .configure("group", this)); } public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy { @Override protected void onEntityChange(Entity member) { if (log.isDebugEnabled()) log.debug("Node {} updated in Cluster {}", member, this); ((CassandraDatacenterImpl)entity).update(); } @Override protected void onEntityAdded(Entity member) { if (log.isDebugEnabled()) log.debug("Node {} added to Cluster {}", member, this); ((CassandraDatacenterImpl)entity).update(); } @Override protected void onEntityRemoved(Entity member) { if (log.isDebugEnabled()) log.debug("Node {} removed from Cluster {}", member, this); ((CassandraDatacenterImpl)entity).update(); } }; @SuppressWarnings("unchecked") protected void connectEnrichers() { List<? extends List<? extends AttributeSensor<? extends Number>>> summingEnricherSetup = ImmutableList.of( ImmutableList.of(CassandraNode.READ_ACTIVE, READ_ACTIVE), ImmutableList.of(CassandraNode.READ_PENDING, READ_PENDING), ImmutableList.of(CassandraNode.WRITE_ACTIVE, WRITE_ACTIVE), ImmutableList.of(CassandraNode.WRITE_PENDING, WRITE_PENDING) ); List<? extends List<? extends AttributeSensor<? extends Number>>> averagingEnricherSetup = ImmutableList.of( ImmutableList.of(CassandraNode.READS_PER_SECOND_LAST, READS_PER_SECOND_LAST_PER_NODE), ImmutableList.of(CassandraNode.WRITES_PER_SECOND_LAST, WRITES_PER_SECOND_LAST_PER_NODE), ImmutableList.of(CassandraNode.WRITES_PER_SECOND_IN_WINDOW, WRITES_PER_SECOND_IN_WINDOW_PER_NODE), ImmutableList.of(CassandraNode.READS_PER_SECOND_IN_WINDOW, READS_PER_SECOND_IN_WINDOW_PER_NODE), ImmutableList.of(CassandraNode.THRIFT_PORT_LATENCY, THRIFT_PORT_LATENCY_PER_NODE), ImmutableList.of(CassandraNode.THRIFT_PORT_LATENCY_IN_WINDOW, THRIFT_PORT_LATENCY_IN_WINDOW_PER_NODE), ImmutableList.of(CassandraNode.PROCESS_CPU_TIME_FRACTION_LAST, PROCESS_CPU_TIME_FRACTION_LAST_PER_NODE), ImmutableList.of(CassandraNode.PROCESS_CPU_TIME_FRACTION_IN_WINDOW, PROCESS_CPU_TIME_FRACTION_IN_WINDOW_PER_NODE) ); for (List<? extends AttributeSensor<? extends Number>> es : summingEnricherSetup) { AttributeSensor<? extends Number> t = es.get(0); AttributeSensor<? extends Number> total = es.get(1); enrichers().add(Enrichers.builder() .aggregating(t) .publishing(total) .fromMembers() .computingSum() .defaultValueForUnreportedSensors(null) .valueToReportIfNoSensors(null) .build()); } for (List<? extends AttributeSensor<? extends Number>> es : averagingEnricherSetup) { AttributeSensor<Number> t = (AttributeSensor<Number>) es.get(0); AttributeSensor<Double> average = (AttributeSensor<Double>) es.get(1); enrichers().add(Enrichers.builder() .aggregating(t) .publishing(average) .fromMembers() .computingAverage() .defaultValueForUnreportedSensors(null) .valueToReportIfNoSensors(null) .build()); } } @Override public void stop() { disconnectSensors(); super.stop(); } protected void disconnectSensors() { } @Override public void update() { synchronized (mutex) { // Update our seeds, as necessary seedTracker.refreshSeeds(); // Choose the first available cluster member to set host and port (and compute one-up) Optional<Entity> upNode = Iterables.tryFind(getMembers(), EntityPredicates.attributeEqualTo(SERVICE_UP, Boolean.TRUE)); if (upNode.isPresent()) { sensors().set(HOSTNAME, upNode.get().getAttribute(Attributes.HOSTNAME)); sensors().set(THRIFT_PORT, upNode.get().getAttribute(CassandraNode.THRIFT_PORT)); List<String> currentNodes = getAttribute(CASSANDRA_CLUSTER_NODES); Set<String> oldNodes = (currentNodes != null) ? ImmutableSet.copyOf(currentNodes) : ImmutableSet.<String>of(); Set<String> newNodes = MutableSet.<String>of(); for (Entity member : getMembers()) { if (member instanceof CassandraNode && Boolean.TRUE.equals(member.getAttribute(SERVICE_UP))) { String hostname = member.getAttribute(Attributes.HOSTNAME); Integer thriftPort = member.getAttribute(CassandraNode.THRIFT_PORT); if (hostname != null && thriftPort != null) { newNodes.add(HostAndPort.fromParts(hostname, thriftPort).toString()); } } } if (Sets.symmetricDifference(oldNodes, newNodes).size() > 0) { sensors().set(CASSANDRA_CLUSTER_NODES, MutableList.copyOf(newNodes)); } } else { sensors().set(HOSTNAME, null); sensors().set(THRIFT_PORT, null); sensors().set(CASSANDRA_CLUSTER_NODES, Collections.<String>emptyList()); } ServiceNotUpLogic.updateNotUpIndicatorRequiringNonEmptyList(this, CASSANDRA_CLUSTER_NODES); } } /** * For tracking our seeds. This gets fiddly! High-level logic is: * <ul> * <li>If we have never reached quorum (i.e. have never published seeds), then continue to wait for quorum; * because entity-startup may be blocking for this. This is handled by the seedSupplier. * <li>If we previously reached quorum (i.e. have previousy published seeds), then always update; * we never want stale/dead entities listed in our seeds. * <li>If an existing seed looks unhealthy, then replace it. * <li>If a new potential seed becomes available (and we're in need of more), then add it. * <ul> * * Also note that {@link CassandraFabric} can take over, because it know about multiple sub-clusters! * It will provide a different {@link CassandraDatacenter#SEED_SUPPLIER}. Each time we think that our seeds * need to change, we call that. The fabric will call into {@link CassandraDatacenterImpl#gatherPotentialSeeds()} * to find out what's available. * * @author aled */ protected class SeedTracker { private final Map<Entity, Boolean> memberUpness = Maps.newLinkedHashMap(); public void onMemberRemoved(Entity member) { Set<Entity> seeds = getSeeds(); boolean maybeRemove = seeds.contains(member); memberUpness.remove(member); if (maybeRemove) { refreshSeeds(); } else { if (log.isTraceEnabled()) log.trace("Seeds considered stable for cluster {} (node {} removed)", new Object[] {CassandraDatacenterImpl.this, member}); return; } } public void onHostnameChanged(Entity member, String hostname) { Set<Entity> seeds = getSeeds(); int quorum = getSeedQuorumSize(); boolean isViable = isViableSeed(member); boolean maybeAdd = isViable && seeds.size() < quorum; boolean maybeRemove = seeds.contains(member) && !isViable; if (maybeAdd || maybeRemove) { refreshSeeds(); } else { if (log.isTraceEnabled()) log.trace("Seeds considered stable for cluster {} (node {} changed hostname {})", new Object[] {CassandraDatacenterImpl.this, member, hostname}); return; } } public void onServiceUpChanged(Entity member, Boolean serviceUp) { Boolean oldVal = memberUpness.put(member, serviceUp); if (Objects.equal(oldVal, serviceUp)) { if (log.isTraceEnabled()) log.trace("Ignoring duplicate service-up in "+CassandraDatacenterImpl.this+" for "+member+", "+serviceUp); } Set<Entity> seeds = getSeeds(); int quorum = getSeedQuorumSize(); boolean isViable = isViableSeed(member); boolean maybeAdd = isViable && seeds.size() < quorum; boolean maybeRemove = seeds.contains(member) && !isViable; if (log.isDebugEnabled()) log.debug("Considering refresh of seeds for "+CassandraDatacenterImpl.this+" because "+member+" is now "+serviceUp+" ("+isViable+" / "+maybeAdd+" / "+maybeRemove+")"); if (maybeAdd || maybeRemove) { refreshSeeds(); } else { if (log.isTraceEnabled()) log.trace("Seeds considered stable for cluster {} (node {} changed serviceUp {})", new Object[] {CassandraDatacenterImpl.this, member, serviceUp}); return; } } protected Set<Entity> getSeeds() { Set<Entity> result = getAttribute(CURRENT_SEEDS); return (result == null) ? ImmutableSet.<Entity>of() : result; } public void refreshSeeds() { Set<Entity> oldseeds = getAttribute(CURRENT_SEEDS); Set<Entity> newseeds = getSeedSupplier().get(); if (Objects.equal(oldseeds, newseeds)) { if (log.isTraceEnabled()) log.debug("Seed refresh no-op for cluster {}: still={}", new Object[] {CassandraDatacenterImpl.this, oldseeds}); } else { if (log.isDebugEnabled()) log.debug("Refreshing seeds of cluster {}: now={}; old={}", new Object[] {this, newseeds, oldseeds}); sensors().set(CURRENT_SEEDS, newseeds); if (newseeds != null && newseeds.size() > 0) { sensors().set(HAS_PUBLISHED_SEEDS, true); } } } public Set<Entity> gatherPotentialSeeds() { Set<Entity> result = Sets.newLinkedHashSet(); for (Entity member : getMembers()) { if (isViableSeed(member)) { result.add(member); } } if (log.isTraceEnabled()) log.trace("Viable seeds in Cluster {}: {}", new Object[] {result}); return result; } public Set<Entity> gatherPotentialRunningSeeds() { Set<Entity> result = Sets.newLinkedHashSet(); for (Entity member : getMembers()) { if (isRunningSeed(member)) { result.add(member); } } if (log.isTraceEnabled()) log.trace("Viable running seeds in Cluster {}: {}", new Object[] {result}); return result; } public boolean isViableSeed(Entity member) { // TODO would be good to reuse the better logic in ServiceFailureDetector // (e.g. if that didn't just emit a notification but set a sensor as well?) boolean managed = Entities.isManaged(member); String hostname = member.getAttribute(Attributes.HOSTNAME); boolean serviceUp = Boolean.TRUE.equals(member.getAttribute(Attributes.SERVICE_UP)); Lifecycle serviceState = member.getAttribute(Attributes.SERVICE_STATE_ACTUAL); boolean hasFailed = !managed || (serviceState == Lifecycle.ON_FIRE) || (serviceState == Lifecycle.RUNNING && !serviceUp) || (serviceState == Lifecycle.STOPPED); boolean result = (hostname != null && !hasFailed); if (log.isTraceEnabled()) log.trace("Node {} in Cluster {}: viableSeed={}; hostname={}; serviceUp={}; serviceState={}; hasFailed={}", new Object[] {member, this, result, hostname, serviceUp, serviceState, hasFailed}); return result; } public boolean isRunningSeed(Entity member) { boolean viableSeed = isViableSeed(member); boolean serviceUp = Boolean.TRUE.equals(member.getAttribute(Attributes.SERVICE_UP)); Lifecycle serviceState = member.getAttribute(Attributes.SERVICE_STATE_ACTUAL); boolean result = viableSeed && serviceUp && serviceState == Lifecycle.RUNNING; if (log.isTraceEnabled()) log.trace("Node {} in Cluster {}: runningSeed={}; viableSeed={}; serviceUp={}; serviceState={}", new Object[] {member, this, result, viableSeed, serviceUp, serviceState}); return result; } } @Override public String executeScript(String commands) { Entity someChild = Iterables.getFirst(getMembers(), null); if (someChild==null) throw new IllegalStateException("No Cassandra nodes available"); // FIXME cross-etntity method-style calls such as below do not set up a queueing context (DynamicSequentialTask) // return ((CassandraNode)someChild).executeScript(commands); return Entities.invokeEffector(this, someChild, CassandraNode.EXECUTE_SCRIPT, MutableMap.of("commands", commands)).getUnchecked(); } }