/*
* 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.core.mgmt.ha;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
import org.apache.brooklyn.api.mgmt.ha.ManagementNodeSyncRecord;
import org.apache.brooklyn.api.mgmt.ha.ManagementPlaneSyncRecord;
import org.apache.brooklyn.api.objs.Identifiable;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.text.NaturalOrderComparator;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;
/**
* @since 0.7.0
*
* @author aled
*/
@Beta
public abstract class BasicMasterChooser implements MasterChooser {
private static final Logger LOG = LoggerFactory.getLogger(BasicMasterChooser.class);
public static class ScoredRecord<T extends Comparable<T>> implements Identifiable, Comparable<ScoredRecord<T>> {
String id;
ManagementNodeSyncRecord record;
T score;
@Override
public String getId() {
return id;
}
@Override
public int compareTo(ScoredRecord<T> o) {
return score.compareTo(o.score);
}
}
public ManagementNodeSyncRecord choose(ManagementPlaneSyncRecord memento, Duration heartbeatTimeout, String ownNodeId) {
if (LOG.isDebugEnabled()) LOG.debug("Choosing new master from "+memento.getManagementNodes());
ManagementNodeSyncRecord me = memento.getManagementNodes().get(ownNodeId);
if (me==null) {
LOG.warn("Management node details not known when choosing new master: "+memento+" / "+ownNodeId);
return null;
}
Long nowIsh = me.getRemoteTimestamp();
if (nowIsh==null) {
LOG.warn("Management node for self missing timestamp when choosing new master: "+memento);
return null;
}
List<ScoredRecord<?>> contenders = filterHealthy(memento, heartbeatTimeout, nowIsh);
if (!contenders.isEmpty()) {
return pick(contenders);
} else {
LOG.info("No valid management node found for choosing new master: contender="+memento.getManagementNodes());
return null;
}
}
/** pick the best contender; argument guaranteed to be non-null and non-empty,
* filtered for health reasons */
@SuppressWarnings({ "rawtypes", "unchecked" })
protected ManagementNodeSyncRecord pick(List<ScoredRecord<?>> contenders) {
ScoredRecord min = null;
for (ScoredRecord x: contenders) {
if (min==null || x.score.compareTo(min.score)<0) min = x;
}
return min.record;
}
public static class AlphabeticChooserScore implements Comparable<AlphabeticChooserScore> {
long priority;
int versionBias;
String brooklynVersion;
int statePriority;
String nodeId;
@Override
public int compareTo(AlphabeticChooserScore o) {
if (o==null) return -1;
return ComparisonChain.start()
// invert the order where we prefer higher values
.compare(o.priority, this.priority)
.compare(o.versionBias, this.versionBias)
.compare(o.brooklynVersion, this.brooklynVersion,
Ordering.from(NaturalOrderComparator.INSTANCE).nullsFirst())
.compare(o.statePriority, this.statePriority)
.compare(this.nodeId, o.nodeId, Ordering.usingToString().nullsLast())
.result();
}
}
/** comparator which prefers, in order:
* <li> higher explicit priority
* <li> non-snapshot Brooklyn version, then any Brooklyn version, and lastly null version
* (using {@link NaturalOrderComparator} so e.g. v10 > v3.20 > v3.9 )
* <li> higher version (null last)
* <li> node which reports it's master, hot standby, then standby
* <li> finally (common case): lower (alphabetically) node id
*/
public static class AlphabeticMasterChooser extends BasicMasterChooser {
final boolean preferHotStandby;
public AlphabeticMasterChooser(boolean preferHotStandby) { this.preferHotStandby = preferHotStandby; }
public AlphabeticMasterChooser() { this.preferHotStandby = true; }
@Override
protected AlphabeticChooserScore score(ManagementNodeSyncRecord contender) {
AlphabeticChooserScore score = new AlphabeticChooserScore();
score.priority = contender.getPriority()!=null ? contender.getPriority() : 0;
score.brooklynVersion = contender.getBrooklynVersion();
score.versionBias = contender.getBrooklynVersion()==null ? -2 :
contender.getBrooklynVersion().toLowerCase().indexOf("snapshot")>=0 ? -1 :
0;
if (preferHotStandby) {
// other master should be preferred before we get invoked, but including for good measure
score.statePriority = contender.getStatus()==ManagementNodeState.MASTER ? 3 :
contender.getStatus()==ManagementNodeState.HOT_STANDBY ? 2 :
contender.getStatus()==ManagementNodeState.STANDBY ? 1 : -1;
} else {
score.statePriority = 0;
}
score.nodeId = contender.getNodeId();
return score;
}
}
/**
* Filters the {@link ManagementPlaneSyncRecord#getManagementNodes()} to only those in an appropriate state,
* and with heartbeats that have not timed out.
*/
protected List<ScoredRecord<?>> filterHealthy(ManagementPlaneSyncRecord memento, Duration heartbeatTimeout, long nowIsh) {
long oldestAcceptableTimestamp = nowIsh - heartbeatTimeout.toMilliseconds();
List<ScoredRecord<?>> contenders = MutableList.of();
for (ManagementNodeSyncRecord contender : memento.getManagementNodes().values()) {
boolean statusOk = (contender.getStatus() == ManagementNodeState.STANDBY || contender.getStatus() == ManagementNodeState.HOT_STANDBY || contender.getStatus() == ManagementNodeState.MASTER);
Long remoteTimestamp = contender.getRemoteTimestamp();
boolean heartbeatOk;
if (remoteTimestamp==null) {
throw new IllegalStateException("Missing remote timestamp when performing master election: "+this+" / "+contender);
// if the above exception happens in some contexts we could either fallback to local, or fail:
// remoteTimestamp = contender.getLocalTimestamp();
// or
// heartbeatOk=false;
} else {
heartbeatOk = remoteTimestamp >= oldestAcceptableTimestamp;
}
if (statusOk && heartbeatOk) {
contenders.add(newScoredRecord(contender));
}
if (LOG.isTraceEnabled()) LOG.trace("Filtering choices of new master: contender="+contender+"; statusOk="+statusOk+"; heartbeatOk="+heartbeatOk);
}
return contenders;
}
@VisibleForTesting
//Java 6 compiler workaround, using parameterized types fails
@SuppressWarnings({ "unchecked", "rawtypes" })
protected List<ScoredRecord<?>> sort(List<ScoredRecord<?>> input) {
ArrayList copy = new ArrayList<ScoredRecord<?>>(input);
Collections.sort(copy);
return copy;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected ScoredRecord<?> newScoredRecord(ManagementNodeSyncRecord contender) {
ScoredRecord r = new ScoredRecord();
r.id = contender.getNodeId();
r.record = contender;
r.score = score(contender);
return r;
}
protected abstract Comparable<?> score(ManagementNodeSyncRecord contender);
}