/**
* Copyright (c) 2002-2012 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.cluster.protocol.cluster;
import static org.neo4j.cluster.com.message.Message.internal;
import static org.neo4j.cluster.com.message.Message.respond;
import static org.neo4j.cluster.com.message.Message.timeout;
import static org.neo4j.cluster.com.message.Message.to;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import org.neo4j.cluster.com.message.Message;
import org.neo4j.cluster.com.message.MessageProcessor;
import org.neo4j.cluster.protocol.atomicbroadcast.multipaxos.AtomicBroadcastMessage;
import org.neo4j.cluster.protocol.atomicbroadcast.multipaxos.InstanceId;
import org.neo4j.cluster.protocol.atomicbroadcast.multipaxos.ProposerMessage;
import org.neo4j.cluster.statemachine.State;
/**
* State machine for the Cluster API
*
* @see Cluster
* @see ClusterMessage
*/
public enum ClusterState
implements State<ClusterContext, ClusterMessage>
{
start
{
@Override
public State<?, ?> handle( ClusterContext context, Message<ClusterMessage> message,
MessageProcessor outgoing ) throws Throwable
{
switch ( message.getMessageType() )
{
case addClusterListener:
{
context.addClusterListener( message.<ClusterListener>getPayload() );
break;
}
case removeClusterListener:
{
context.removeClusterListener( message.<ClusterListener>getPayload() );
break;
}
case create:
{
String name = message.getPayload();
context.getLogger().info( "Creating cluster: " + name );
context.created( name );
return entered;
}
case join:
{
URI clusterNodeUri = message.getPayload();
outgoing.process( to( ClusterMessage.configurationRequest, clusterNodeUri ) );
context.timeouts.setTimeout( clusterNodeUri,
timeout( ClusterMessage.configurationTimeout, message, clusterNodeUri ) );
return acquiringConfiguration;
}
}
return this;
}
},
acquiringConfiguration
{
@Override
public State<?, ?> handle( ClusterContext context, Message<ClusterMessage> message,
MessageProcessor outgoing ) throws Throwable
{
switch ( message.getMessageType() )
{
case configurationResponse:
{
context.timeouts.cancelTimeout( new URI( message.getHeader( Message.FROM ) ) );
ClusterMessage.ConfigurationResponseState state = message.getPayload();
context.getLogger().info( "Joining cluster "+state.getClusterName() );
if (!context.getConfiguration().getName().equals( state.getClusterName() ))
{
context.getLogger().warn( "Joined cluster name is different than the one configured." +
"Expected " + context.getConfiguration().getName() + ", got "
+ state.getClusterName() + ".");
}
List<URI> memberList = new ArrayList<URI>( state.getMembers() );
if ( !memberList.contains( context.me ) )
{
context.learnerContext.setLastDeliveredInstanceId( state.getLatestReceivedInstanceId
().getId() );
context.learnerContext.learnedInstanceId( state.getLatestReceivedInstanceId().getId() );
context.proposerContext.lastInstanceId = state.getLatestReceivedInstanceId().getId()
+ 1;
context.acquiredConfiguration( memberList, state.getRoles() );
context.getLogger().info( String.format( "%s joining:%s, last delivered:%d",
context.me.toString(), context.getConfiguration().toString(),
state.getLatestReceivedInstanceId().getId() ) );
ClusterMessage.ConfigurationChangeState newState = new ClusterMessage
.ConfigurationChangeState();
newState.join( context.me );
// Let the coordinator propose this if possible
URI coordinator = state.getRoles().get( ClusterConfiguration.COORDINATOR );
if ( coordinator != null )
{
outgoing.process( to( ProposerMessage.propose, coordinator, newState ) );
}
else
{
outgoing.process( to( ProposerMessage.propose, new URI( message.getHeader(
Message.FROM ) ), newState ) );
}
context.getLogger().info( "Setup join timeout for " + message.getHeader( Message
.CONVERSATION_ID ) );
context.timeouts.setTimeout( "join", timeout( ClusterMessage.joiningTimeout, message,
new URI( message.getHeader( Message.FROM ) ) ) );
return joining;
}
else
{
// Already in (probably due to crash of this server previously), go to entered state
context.learnerContext.setLastDeliveredInstanceId( state.getLatestReceivedInstanceId
().getId() );
context.learnerContext.learnedInstanceId( state.getLatestReceivedInstanceId().getId() );
context.proposerContext.lastInstanceId = state.getLatestReceivedInstanceId().getId()
+ 1;
context.acquiredConfiguration( memberList, state.getRoles() );
context.joined();
outgoing.process( internal( ClusterMessage.joinResponse, context.getConfiguration() ) );
return entered;
}
}
case configurationTimeout:
{
// The member I got the configuration from, put it last on the list
// and then try the member at the top of the list
List<URI> members = context.getConfiguration().getMembers();
members.remove( message.<URI>getPayload() );
if ( context.getConfiguration().getMembers().isEmpty() )
{
outgoing.process( internal( ClusterMessage.joinFailure,
new TimeoutException( "Join failed, timeout waiting for configuration" ) ) );
return start;
}
else
{
URI nextMember = members.get( 0 );
outgoing.process( internal( ClusterMessage.join, nextMember ) );
return start;
}
}
}
return this;
}
},
joining
{
@Override
public State<?, ?> handle( ClusterContext context,
Message<ClusterMessage> message,
MessageProcessor outgoing
)
throws Throwable
{
switch ( message.getMessageType() )
{
case configurationChanged:
{
ClusterMessage.ConfigurationChangeState state = message.getPayload();
if ( context.getMe().equals( state.getJoin() ) )
{
context.timeouts.cancelTimeout( "join" );
context.joined();
outgoing.process( internal( ClusterMessage.joinResponse, context.getConfiguration() ) );
return entered;
}
else
{
state.apply( context );
return this;
}
}
case joiningTimeout:
{
context.getLogger().info( "Join timeout for " + message.getHeader( Message
.CONVERSATION_ID ) );
// The member I got the configuration from, put it last on the list
// and then try the member at the top of the list
List<URI> members = context.getConfiguration().getMembers();
members.remove( message.<URI>getPayload() );
members.add( message.<URI>getPayload() );
URI nextMember = members.get( 0 );
outgoing.process( internal( ClusterMessage.join, nextMember ) );
return start;
}
case joinFailure:
{
// This causes an exception from the join() method
return start;
}
}
return this;
}
},
entered
{
@Override
public State<?, ?> handle( ClusterContext context, Message<ClusterMessage> message,
MessageProcessor outgoing ) throws Throwable
{
switch ( message.getMessageType() )
{
case addClusterListener:
{
context.addClusterListener( message.<ClusterListener>getPayload() );
break;
}
case removeClusterListener:
{
context.removeClusterListener( message.<ClusterListener>getPayload() );
break;
}
case configurationRequest:
{
outgoing.process( respond( ClusterMessage.configurationResponse, message,
new ClusterMessage.ConfigurationResponseState( context.getConfiguration()
.getRoles(),
context.getConfiguration().getMembers(),
new InstanceId( context.learnerContext.getLastDeliveredInstanceId() ),
context.getConfiguration().getName() ) ) );
break;
}
case configurationChanged:
{
ClusterMessage.ConfigurationChangeState state = message.getPayload();
state.apply( context );
break;
}
case leave:
{
List<URI> nodeList = new ArrayList<URI>( context.getConfiguration().getMembers() );
if ( nodeList.size() == 1 )
{
context.getLogger().info( "Shutting down cluster: " + context
.getConfiguration().getName() );
context.left();
return start;
}
else
{
context.getLogger().info( "Leaving:" + nodeList );
ClusterMessage.ConfigurationChangeState newState = new ClusterMessage
.ConfigurationChangeState();
newState.leave( context.me );
outgoing.process( internal( AtomicBroadcastMessage.broadcast, newState ) );
context.timeouts.setTimeout( "leave", timeout( ClusterMessage.leaveTimedout,
message ) );
return leaving;
}
}
}
return this;
}
},
leaving
{
@Override
public State<?, ?> handle( ClusterContext context,
Message<ClusterMessage> message,
MessageProcessor outgoing
)
throws Throwable
{
switch ( message.getMessageType() )
{
case configurationChanged:
{
ClusterMessage.ConfigurationChangeState state = message.getPayload();
if ( state.isLeaving( context.getMe() ) )
{
context.timeouts.cancelTimeout( "leave" );
context.left();
return start;
}
else
{
state.apply( context );
return leaving;
}
}
case leaveTimedout:
{
context.getLogger().warn( "Failed to leave. Cluster may consider this instance still a " +
"member" );
context.left();
return start;
}
}
return this;
}
}
}