package org.jgroups.blocks;
import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.jgroups.*;
import org.jgroups.util.Rsp;
import org.jgroups.util.RspList;
import java.io.Serializable;
import java.util.*;
/**
* Voting adapter provides a voting functionality for an application. There
* should be at most one {@link VotingAdapter} listening on one {@link Channel}
* instance. Each adapter can have zero or more registered {@link VotingListener}
* instances that will be called during voting process.
* <p>
* Decree is an object that has some semantic meaning within the application.
* Each voting listener receives a decree and can respond with either
* <code>true</code> or false. If the decree has no meaning for the voting
* listener, it is required to throw {@link VoteException}. In this case
* this specific listener will be excluded from the voting on the specified
* decree. After performing local voting, this voting adapter sends the request
* back to the originator of the voting process. Originator receives results
* from each node and decides if all voting process succeeded or not depending
* on the consensus type specified during voting.
*
* @author Roman Rokytskyy (rrokytskyy@acm.org)
* @author Robert Schaffar-Taurok (robert@fusion.at)
*/
public class VotingAdapter implements MessageListener, MembershipListener, VoteResponseProcessor {
/**
* This consensus type means that at least one positive vote is required
* for the voting to succeed.
*/
public static final int VOTE_ANY = 0;
/**
* This consensus type means that at least one positive vote and no negative
* votes are required for the voting to succeed.
*/
public static final int VOTE_ALL = 1;
/**
* This consensus type means that number of positive votes should be greater
* than number of negative votes.
*/
public static final int VOTE_MAJORITY = 2;
private static final int PROCESS_CONTINUE = 0;
private static final int PROCESS_SKIP = 1;
private static final int PROCESS_BREAK = 2;
private final RpcDispatcher rpcDispatcher;
protected final Log log=LogFactory.getLog(getClass());
private final HashSet suspectedNodes = new HashSet();
private boolean closed;
private final List membership_listeners=new LinkedList();
/**
* Creates an instance of the VoteChannel that uses JGroups
* for communication between group members.
* @param channel JGroups channel.
*/
public VotingAdapter(Channel channel) {
rpcDispatcher = new RpcDispatcher(channel, this, this, this);
}
public VotingAdapter(PullPushAdapter adapter, Serializable id) {
rpcDispatcher = new RpcDispatcher(adapter, id, this, this, this);
}
public Collection getMembers() {
return rpcDispatcher != null? rpcDispatcher.getMembers() : null;
}
public void addMembershipListener(MembershipListener l) {
if(l != null && !membership_listeners.contains(l))
membership_listeners.add(l);
}
public void removeMembershipListener(MembershipListener l) {
if(l != null)
membership_listeners.remove(l);
}
/**
* Performs actual voting on the VoteChannel using the JGroups
* facilities for communication.
*/
public boolean vote(Object decree, int consensusType, long timeout)
throws ChannelException {
return vote(decree, consensusType, timeout, null);
}
/**
* Performs actual voting on the VoteChannel using the JGroups
* facilities for communication.
*/
public boolean vote(Object decree, int consensusType, long timeout, VoteResponseProcessor voteResponseProcessor)
throws ChannelException
{
if (closed)
throw new ChannelException("Channel was closed.");
if(log.isDebugEnabled()) log.debug("Conducting voting on decree " + decree + ", consensus type " +
getConsensusStr(consensusType) + ", timeout " + timeout);
int mode = GroupRequest.GET_ALL;
// perform the consensus mapping
switch (consensusType) {
case VotingAdapter.VOTE_ALL : mode = GroupRequest.GET_ALL; break;
case VotingAdapter.VOTE_ANY : mode = GroupRequest.GET_FIRST; break;
case VotingAdapter.VOTE_MAJORITY : mode = GroupRequest.GET_MAJORITY; break;
default : mode = GroupRequest.GET_ALL;
}
try {
java.lang.reflect.Method method = this.getClass().getMethod(
"localVote", new Class[] { Object.class });
MethodCall methodCall = new MethodCall(method, new Object[] {decree});
if(log.isDebugEnabled()) log.debug("Calling remote methods...");
// vote
RspList responses = rpcDispatcher.callRemoteMethods(
null, methodCall, mode, timeout);
if(log.isDebugEnabled()) log.debug("Checking responses.");
if (voteResponseProcessor == null) {
voteResponseProcessor = this;
}
return voteResponseProcessor.processResponses(responses, consensusType, decree);
} catch(NoSuchMethodException nsmex) {
// UPS!!! How can this happen?!
if(log.isErrorEnabled()) log.error("Could not find method localVote(Object). " +
nsmex.toString());
throw new UnsupportedOperationException(
"Cannot execute voting because of absence of " +
this.getClass().getName() + ".localVote(Object) method.");
}
}
/**
* Processes the response list and makes a decision according to the
* type of the consensus for current voting.
* <p>
* Note: we do not support voting in case of Byzantine failures, i.e.
* when the node responds with the fault message.
*/
public boolean processResponses(RspList responses, int consensusType, Object decree)
throws ChannelException
{
if (responses == null) {
return false;
}
boolean voteResult = false;
int totalPositiveVotes = 0;
int totalNegativeVotes = 0;
for(Iterator it=responses.values().iterator(); it.hasNext();) {
Rsp response = (Rsp)it.next();
switch(checkResponse(response)) {
case PROCESS_SKIP : continue;
case PROCESS_BREAK : return false;
}
VoteResult result = (VoteResult)response.getValue();
totalPositiveVotes += result.getPositiveVotes();
totalNegativeVotes += result.getNegativeVotes();
}
switch(consensusType) {
case VotingAdapter.VOTE_ALL :
voteResult = (totalNegativeVotes == 0 && totalPositiveVotes > 0);
break;
case VotingAdapter.VOTE_ANY :
voteResult = (totalPositiveVotes > 0);
break;
case VotingAdapter.VOTE_MAJORITY :
voteResult = (totalPositiveVotes > totalNegativeVotes);
}
return voteResult;
}
/**
* This method checks the response and says the processResponses() method
* what to do.
* @return PROCESS_CONTINUE to continue calculating votes,
* PROCESS_BREAK to stop calculating votes from the nodes,
* PROCESS_SKIP to skip current response.
* @throws ChannelException when the response is fatal to the
* current voting process.
*/
private int checkResponse(Rsp response) throws ChannelException {
if (!response.wasReceived()) {
if(log.isDebugEnabled()) log.debug("Response from node " + response.getSender() +
" was not received.");
// what do we do when one node failed to respond?
//throw new ChannelException("Node " + response.GetSender() +
// " failed to respond.");
return PROCESS_BREAK ;
}
/**@todo check what to do here */
if (response.wasSuspected()) {
if(log.isDebugEnabled()) log.debug("Node " + response.getSender() + " was suspected.");
// wat do we do when one node is suspected?
return PROCESS_SKIP ;
}
Object object = response.getValue();
// we received exception/error, something went wrong
// on one of the nodes... and we do not handle such faults
if (object instanceof Throwable) {
throw new ChannelException("Node " + response.getSender() +
" is faulty.");
}
if (object == null) {
return PROCESS_SKIP;
}
// it is always interesting to know the class that caused failure...
if (!(object instanceof VoteResult)) {
String faultClass = object.getClass().getName();
// ...but we do not handle byzantine faults
throw new ChannelException("Node " + response.getSender() +
" generated fault (class " + faultClass + ')');
}
// what if we received the response from faulty node?
if (object instanceof FailureVoteResult) {
if(log.isErrorEnabled()) log.error(((FailureVoteResult)object).getReason());
return PROCESS_BREAK;
}
// everything is fine :)
return PROCESS_CONTINUE;
}
/**
* Callback for notification about the new view of the group.
*/
public void viewAccepted(View newView) {
// clean nodes that were suspected but still exist in new view
Iterator iterator = suspectedNodes.iterator();
while(iterator.hasNext()) {
Address suspectedNode = (Address)iterator.next();
if (newView.containsMember(suspectedNode))
iterator.remove();
}
for(Iterator it=membership_listeners.iterator(); it.hasNext();) {
MembershipListener listener=(MembershipListener)it.next();
try {
listener.viewAccepted(newView);
}
catch(Throwable t) {
if(log.isErrorEnabled())
log.error("failed calling viewAccepted() on " + listener, t);
}
}
}
/**
* Callback for notification that one node is suspected
*/
public void suspect(Address suspected) {
suspectedNodes.add(suspected);
for(Iterator it=membership_listeners.iterator(); it.hasNext();) {
MembershipListener listener=(MembershipListener)it.next();
try {
listener.suspect(suspected);
}
catch(Throwable t) {
if(log.isErrorEnabled())
log.error("failed calling suspect() on " + listener, t);
}
}
}
/**
* Blocks the channel until the ViewAccepted is invoked.
*/
public void block() {
for(Iterator it=membership_listeners.iterator(); it.hasNext();) {
MembershipListener listener=(MembershipListener)it.next();
try {
listener.block();
}
catch(Throwable t) {
if(log.isErrorEnabled())
log.error("failed calling block() on " + listener, t);
}
}
}
/**
* Get the channel state.
*
* @return always <code>null</code>, we do not have any group-shared
* state.
*/
public byte[] getState() {
return null;
}
/**
* Receive the message. All messages are ignored.
*
* @param msg message to check.
*/
public void receive(org.jgroups.Message msg) {
// do nothing
}
/**
* Set the channel state. We do nothing here.
*/
public void setState(byte[] state) {
// ignore the state, we do not have any.
}
private final Set voteListeners = new HashSet();
private VotingListener[] listeners;
/**
* Vote on the specified decree requiring all nodes to vote.
*
* @param decree decree on which nodes should vote.
* @param timeout time during which nodes can vote.
*
* @return <code>true</code> if nodes agreed on a decree, otherwise
* <code>false</code>
*
* @throws ChannelException if something went wrong.
*/
public boolean vote(Object decree, long timeout) throws ChannelException {
return vote(decree, timeout, null);
}
/**
* Vote on the specified decree requiring all nodes to vote.
*
* @param decree decree on which nodes should vote.
* @param timeout time during which nodes can vote.
* @param voteResponseProcessor processor which will be called for every response that is received.
*
* @return <code>true</code> if nodes agreed on a decree, otherwise
* <code>false</code>
*
* @throws ChannelException if something went wrong.
*/
public boolean vote(Object decree, long timeout, VoteResponseProcessor voteResponseProcessor) throws ChannelException {
return vote(decree, VOTE_ALL, timeout, voteResponseProcessor);
}
/**
* Adds voting listener.
*/
public void addVoteListener(VotingListener listener) {
voteListeners.add(listener);
listeners = (VotingListener[])voteListeners.toArray(
new VotingListener[voteListeners.size()]);
}
/**
* Removes voting listener.
*/
public void removeVoteListener(VotingListener listener) {
voteListeners.remove(listener);
listeners = (VotingListener[])voteListeners.toArray(
new VotingListener[voteListeners.size()]);
}
/**
* This method performs voting on the specific decree between all
* local voteListeners.
*/
public VoteResult localVote(Object decree) {
VoteResult voteResult = new VoteResult();
for(int i = 0; i < listeners.length; i++) {
VotingListener listener = listeners[i];
try {
voteResult.addVote(listener.vote(decree));
} catch (VoteException vex) {
// do nothing here.
} catch(RuntimeException ex) {
if(log.isErrorEnabled()) log.error(ex.toString());
// if we are here, then listener
// had thrown a RuntimeException
return new FailureVoteResult(ex.getMessage());
}
}
if(log.isDebugEnabled()) log.debug("Voting on decree " + decree.toString() + " : " +
voteResult.toString());
return voteResult;
}
/**
* Convert consensus type into string representation. This method is
* useful for debugginf.
*
* @param consensusType type of the consensus.
*
* @return string representation of the consensus type.
*/
public static String getConsensusStr(int consensusType) {
switch(consensusType) {
case VotingAdapter.VOTE_ALL : return "VOTE_ALL";
case VotingAdapter.VOTE_ANY : return "VOTE_ANY";
case VotingAdapter.VOTE_MAJORITY : return "VOTE_MAJORITY";
default : return "UNKNOWN";
}
}
/**
* This class represents the result of local voting. It contains a
* number of positive and negative votes collected during local voting.
*/
public static class VoteResult implements Serializable {
private int positiveVotes = 0;
private int negativeVotes = 0;
private static final long serialVersionUID = 2868605599965196746L;
public void addVote(boolean vote) {
if (vote)
positiveVotes++;
else
negativeVotes++;
}
public int getPositiveVotes() { return positiveVotes; }
public int getNegativeVotes() { return negativeVotes; }
public String toString() {
return "VoteResult: up=" + positiveVotes +
", down=" + negativeVotes;
}
}
/**
* Class that represents a result of local voting on the failed node.
*/
public static class FailureVoteResult extends VoteResult {
private final String reason;
public FailureVoteResult(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
}