blob: eeb22117daa469f59f3c1c13b9988c72ac121b67 [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.sling.discovery.impl.cluster.voting;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.discovery.commons.providers.util.ResourceHelper;
import org.apache.sling.discovery.impl.Config;
import org.apache.sling.discovery.impl.common.View;
import org.apache.sling.discovery.impl.common.ViewHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DAO for an ongoing voting, providing a few helper methods
*/
public class VotingView extends View {
/**
* use static logger to avoid frequent initialization as is potentially the
* case with ClusterViewResource.
**/
private final static Logger logger = LoggerFactory
.getLogger(VotingView.class);
/**
* Create a new voting with the given list of instances, the given
* voting/view id and the given slingid of the initiator.
* @param newViewId the new voting/view id
* @param initiatorId the slingid of the initiator
* @param liveInstances the list of live instances to add to the voting
* @return a DAO object representing the voting
*/
public static VotingView newVoting(final ResourceResolver resourceResolver,
final Config config,
final String newViewId, String initiatorId, final Set<String> liveInstances) throws PersistenceException {
if (!liveInstances.contains(initiatorId)) {
// SLING-4617 : a voting, on a single instance, was created without the local instance
// this should in no case happen - the local instance should always be part of the live
// instances. if that's not the case, then something's fishy and we should not create
// the new voting - and instead rely on a retry later.
logger.warn("newVoting: liveInstances does not include initiatorId (local instance) - not creating new, invalid, voting");
return null;
}
final Resource votingResource = ResourceHelper.getOrCreateResource(
resourceResolver, config.getOngoingVotingsPath() + "/"
+ newViewId);
final ModifiableValueMap votingMap = votingResource.adaptTo(ModifiableValueMap.class);
votingMap.put("votingStart", Calendar.getInstance());
String clusterId = null;
Calendar clusterIdDefinedAt = null;
String clusterIdDefinedBy = null;
final View currentlyEstablishedView = ViewHelper.getEstablishedView(resourceResolver, config);
if (currentlyEstablishedView != null) {
final ValueMap establishedViewValueMap = currentlyEstablishedView.getResource().adaptTo(ValueMap.class);
clusterId = establishedViewValueMap.get(VIEW_PROPERTY_CLUSTER_ID, String.class);
if (clusterId == null || clusterId.length() == 0) {
clusterId = currentlyEstablishedView.getResource().getName();
}
Date date = establishedViewValueMap.get(VIEW_PROPERTY_CLUSTER_ID_DEFINED_AT, Date.class);
if (date!=null) {
clusterIdDefinedAt = Calendar.getInstance();
clusterIdDefinedAt.setTime(date);
}
clusterIdDefinedBy = establishedViewValueMap.get(VIEW_PROPERTY_CLUSTER_ID_DEFINED_BY, String.class);
}
if (clusterId == null || clusterId.length() == 0) {
clusterId = newViewId;
clusterIdDefinedAt = Calendar.getInstance();
}
votingMap.put(VIEW_PROPERTY_CLUSTER_ID, clusterId);
if (clusterIdDefinedAt != null) {
votingMap.put(VIEW_PROPERTY_CLUSTER_ID_DEFINED_AT, clusterIdDefinedAt);
}
if (clusterIdDefinedBy == null || clusterIdDefinedBy.length() == 0) {
clusterIdDefinedBy = initiatorId;
}
votingMap.put(VIEW_PROPERTY_CLUSTER_ID_DEFINED_BY, clusterIdDefinedBy);
final Resource membersResource = resourceResolver.create(votingResource, "members", null);
final Iterator<String> it = liveInstances.iterator();
while (it.hasNext()) {
String memberId = it.next();
Map<String, Object> properties = new HashMap<String, Object>();
if (memberId.equals(initiatorId)) {
properties.put("initiator", true);
properties.put("vote", true);
properties.put("votedAt", Calendar.getInstance());
}
Resource instanceResource = ResourceHelper.getOrCreateResource(
resourceResolver, config.getClusterInstancesPath() + "/"
+ memberId);
String leaderElectionId = instanceResource.adaptTo(ValueMap.class)
.get("leaderElectionId", String.class);
properties.put("leaderElectionId", leaderElectionId);
resourceResolver.create(membersResource, memberId, properties);
}
logger.debug("newVoting: committing new voting: newViewId="+newViewId+", initiatorId="+initiatorId+", resource="+votingResource+", #members: "+liveInstances.size()+", members: "+liveInstances);
resourceResolver.commit();
logger.info("newVoting: new voting started: newViewId="+newViewId+", initiatorId="+initiatorId+", resource="+votingResource+", #members: "+liveInstances.size()+", members: "+liveInstances);
return new VotingView(votingResource);
}
/**
* Construct a voting view based on the given resource
* @param viewResource the resource which is the place the voting is kept
*/
public VotingView(final Resource viewResource) {
super(viewResource);
}
public String getVotingId() {
return getResource().getName();
}
@Override
public String toString() {
try {
final Resource members = getResource().getChild("members");
String initiatorId = null;
final StringBuilder sb = new StringBuilder();
if (members != null) {
Iterator<Resource> it = members.getChildren().iterator();
while (it.hasNext()) {
Resource r = it.next();
if (sb.length() != 0) {
sb.append(", ");
}
sb.append(r.getName());
ValueMap properties = r.adaptTo(ValueMap.class);
if (properties != null) {
Boolean initiator = properties.get("initiator",
Boolean.class);
if (initiator != null && initiator) {
initiatorId = r.getName();
}
}
}
}
return "a VotingView[viewId=" + getViewId()
+ ", id=" + getResource().getName() + ", initiator="
+ initiatorId + ", members=" + sb + "]";
} catch(Exception e) {
return "a VotingView["+super.toString()+"]";
}
}
/**
* Checks whether this voting is still ongoing - that is, whether
* a valid votingStart is set and whether that's within the heartbeat timeout configured
* @param config
* @return
*/
public boolean isOngoingVoting(final Config config) {
final long votingStart = getVotingStartTime();
if (votingStart==-1) {
return false;
}
final long now = System.currentTimeMillis();
final long diff = now - votingStart;
return diff < config.getHeartbeatTimeoutMillis();
}
/**
* Checks whether this voting has timed out - that is, whether
* there is a valid votingStart set and whether that has timed out
*/
public boolean isTimedoutVoting(final Config config) {
final long votingStart = getVotingStartTime();
if (votingStart==-1) {
return false;
}
final long now = System.currentTimeMillis();
final long diff = now - votingStart;
return diff > config.getHeartbeatTimeoutMillis();
}
/** Get the value of the votingStart property - or -1 if anything goes wrong reading that **/
private long getVotingStartTime() {
ValueMap properties = null;
try{
properties = getResource().adaptTo(ValueMap.class);
} catch(RuntimeException e) {
logger.info("getVotingStartTime: could not get properties of "+getResource()+". Likely in creation: "+e, e);
return -1;
}
if (properties == null) {
// no properties, odd. then it's not a valid voting.
return -1;
}
final Date votingStartDate = properties.get("votingStart", Date.class);
if (votingStartDate == null) {
if (logger.isDebugEnabled()) {
logger.debug("getVotingStartTime: got a voting without votingStart. Likely in creation: "
+ getResource());
}
return -1;
}
final long votingStart = votingStartDate.getTime();
return votingStart;
}
/**
* Checks whether there are any no votes on this voting
* @return true if there are any no votes on this voting
*/
public boolean hasNoVotes() {
Resource m = getResource().getChild("members");
if (m==null) {
// the vote is being created. wait.
return false;
}
final Iterator<Resource> it = m.getChildren()
.iterator();
while (it.hasNext()) {
Resource aMemberRes = it.next();
ValueMap properties = aMemberRes.adaptTo(ValueMap.class);
if (properties == null) {
continue;
}
Boolean vote = properties.get("vote", Boolean.class);
if (vote != null && !vote) {
return true;
}
}
return false;
}
/**
* Checks whether the given slingId has voted yes for this voting
* @param slingId the sling id to check for
* @return true if the given slingId has voted yes for this voting
*/
public boolean hasVotedYes(final String slingId) {
final Boolean vote = getVote(slingId);
return vote != null && vote;
}
/**
* Get the vote of the instance with the given slingId
* @param slingId
* @return null if that instance did not vote yet (or the structure
* is faulty), true if the instance voted yes, false if it voted no
*/
public Boolean getVote(String slingId) {
Resource members = getResource().getChild("members");
if (members==null) {
return null;
}
final Resource memberResource = members.getChild(
slingId);
if (memberResource == null) {
return null;
}
final ValueMap properties = memberResource.adaptTo(ValueMap.class);
if (properties == null) {
return null;
}
final Boolean vote = properties.get("vote", Boolean.class);
return vote;
}
/**
* Checks whether this voting was initiated by the given slingId
* @return whether this voting was initiated by the given slingId
*/
public boolean isInitiatedBy(final String slingId) {
Resource r = getResource();
if (r == null) {
return false;
}
Resource members = r.getChild("members");
if (members == null) {
if (logger.isDebugEnabled()) {
logger.debug("isInitiatedBy: slingId=" + slingId + ", members null!");
}
return false;
}
final Resource memberResource = members.getChild(
slingId);
if (memberResource == null) {
if (logger.isDebugEnabled()) {
logger.debug("isInitiatedBy: slingId=" + slingId + ", memberResource null!");
}
return false;
}
final ValueMap properties = memberResource.adaptTo(ValueMap.class);
if (properties == null) {
if (logger.isDebugEnabled()) {
logger.debug("isInitiatedBy: slingId=" + slingId + ", properties null!");
}
return false;
}
final Boolean initiator = properties.get("initiator", Boolean.class);
boolean result = initiator != null && initiator;
if (logger.isDebugEnabled()) {
logger.debug("isInitiatedBy: slingId=" + slingId + ", initiator=" + initiator + ", result=" + result);
}
return result;
}
/**
* add a vote from the given slingId to this voting
* @param slingId the slingId which is voting
* @param vote true for a yes-vote, false for a no-vote
*/
public void vote(final String slingId, final Boolean vote,
final String leaderElectionId) {
if (logger.isDebugEnabled()) {
logger.debug("vote: slingId=" + slingId + ", vote=" + vote);
}
Resource r = getResource();
if (r == null) {
logger.error("vote: no resource set. slingId = " + slingId + ", vote=" + vote);
return;
}
Resource members = r.getChild("members");
if (members == null) {
logger.error("vote: no members resource available for " + r + ". slingId = " + slingId + ", vote=" + vote);
return;
}
final Resource memberResource = members.getChild(
slingId);
if (memberResource == null) {
if (vote == null || !vote) {
// if I wanted to vote no or empty, then it's no big deal
// that I can't find my entry ..
logger.debug("vote: no memberResource found for slingId=" + slingId
+ ", vote=" + vote + ", resource=" + getResource());
} else {
// if I wanted to vote yes, then it is a big deal that I can't find myself
logger.error("vote: no memberResource found for slingId=" + slingId
+ ", vote=" + vote + ", resource=" + getResource());
}
return;
}
final ModifiableValueMap memberMap = memberResource.adaptTo(ModifiableValueMap.class);
if (vote == null) {
if (memberMap.containsKey("vote")) {
logger.info("vote: removing vote (vote==null) of slingId="+slingId+" on: "+this);
} else {
logger.debug("vote: removing vote (vote==null) of slingId="+slingId+" on: "+this);
}
memberMap.remove("vote");
} else {
boolean shouldVote = true;
try {
if (memberMap.containsKey("vote")) {
Object v = memberMap.get("vote");
if (v instanceof Property) {
Property p = (Property)v;
if (p.getBoolean() == vote) {
logger.debug("vote: already voted, with same vote ("+vote+"), not voting again");
shouldVote = false;
}
} else if (v instanceof Boolean) {
Boolean b = (Boolean)v;
if (b.equals(vote)) {
logger.debug("vote: already voted, with same vote ("+vote+"), not voting again");
shouldVote = false;
}
}
}
} catch (ValueFormatException e) {
logger.warn("vote: got a ValueFormatException: "+e, e);
} catch (RepositoryException e) {
logger.warn("vote: got a RepositoryException: "+e, e);
}
if (shouldVote) {
logger.info("vote: slingId=" + slingId + " is voting vote=" + vote+" on "+getResource());
memberMap.put("vote", vote);
memberMap.put("votedAt", Calendar.getInstance());
String currentLeaderElectionId = memberMap.get("leaderElectionId", String.class);
if (leaderElectionId!=null &&
(currentLeaderElectionId == null || !currentLeaderElectionId.equals(leaderElectionId))) {
// SLING-5030 : to ensure leader-step-down after being
// isolated from the cluster, the leaderElectionId must
// be explicitly set upon voting.
// for 99% of the cases not be necessary,
// for the rejoin-after-isolation case however it is
logger.info("vote: changing leaderElectionId on vote to "+leaderElectionId);
memberMap.put("leaderElectionId", leaderElectionId);
memberMap.put("leaderElectionIdCreatedAt", new Date());
}
}
}
try {
getResource().getResourceResolver().commit();
} catch (PersistenceException e) {
logger.error("vote: PersistenceException while voting: "+e, e);
}
}
/**
* Checks whether this voting is winning - winning is when it has
* votes from each of the members and all are yes votes
* @return true if this voting is winning
*/
public boolean isWinning() {
final Resource members = getResource().getChild("members");
if (members==null) {
// the vote is being created. wait.
return false;
}
try{
final Iterable<Resource> children = members.getChildren();
final Iterator<Resource> it = children.iterator();
boolean isWinning = false;
while (it.hasNext()) {
Resource aMemberRes = it.next();
try{
ValueMap properties = aMemberRes.adaptTo(ValueMap.class);
Boolean vote = properties.get("vote", Boolean.class);
if (vote != null && vote) {
isWinning = true;
continue;
}
return false;
} catch(RuntimeException re) {
logger.info("isWinning: Could not check vote due to "+re);
return false;
}
}
return isWinning;
} catch(RuntimeException re) {
// SLING-2945: gracefully handle case where members node is
// deleted by another instance
logger.info("isWinning: could not check vote due to "+re);
return false;
}
}
/**
* Checks if this voting matches the current live view
* @throws Exception when something failed during matching
*/
public String matchesLiveView(final Config config) throws Exception {
Resource clusterNodesRes = getResource().getResourceResolver()
.getResource(config.getClusterInstancesPath());
if (clusterNodesRes == null) {
throw new Exception("no clusterNodesRes["+getResource()+"]");
}
return matchesLiveView(clusterNodesRes, config);
}
}