/**
* 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.omega;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.neo4j.cluster.com.message.Message;
import org.neo4j.cluster.com.message.MessageProcessor;
import org.neo4j.cluster.protocol.omega.payload.CollectPayload;
import org.neo4j.cluster.protocol.omega.payload.CollectResponsePayload;
import org.neo4j.cluster.protocol.omega.payload.RefreshAckPayload;
import org.neo4j.cluster.protocol.omega.payload.RefreshPayload;
import org.neo4j.cluster.protocol.omega.state.EpochNumber;
import org.neo4j.cluster.protocol.omega.state.State;
import org.neo4j.cluster.protocol.omega.state.View;
public class OmegaStateTest
{
@Test
public void testStartTransition() throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Message.internal( OmegaMessage.start );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
OmegaState result = (OmegaState) OmegaState.start.handle( context, message, outgoing );
// Assert we move to operational state
assertEquals( OmegaState.omega, result );
// And that timers started
Mockito.verify( context ).startTimers();
}
@Test
public void testRefreshTimeoutResponse() throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Message.internal( OmegaMessage.refresh_timeout );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
State state = new State( new EpochNumber() );
Mockito.when( context.getMyState() ).thenReturn( state );
Set<URI> servers = new HashSet<URI>();
servers.add( new URI( "localhost:80" ) );
servers.add( new URI( "localhost:81" ) );
Mockito.when( context.getServers() ).thenReturn( (Collection) servers );
OmegaState result = (OmegaState) OmegaState.omega.handle( context, message, outgoing );
assertEquals( OmegaState.omega, result );
Mockito.verify( context ).getServers();
Mockito.verify( outgoing, Mockito.times( servers.size() ) ).process( Matchers.isA( Message.class ) );
Mockito.verify( context ).startRefreshRound();
}
@Test
public void testRefreshSuccess() throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Message.internal( OmegaMessage.refresh_ack, RefreshAckPayload.forRefresh( new
RefreshPayload( 1, 2, 3, 1 ) ) );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
Mockito.when( context.getClusterNodeCount() ).thenReturn( 3 );
Mockito.when( context.getAckCount( 1 ) ).thenReturn( 2 );
State state = new State( new EpochNumber() );
Mockito.when( context.getMyState() ).thenReturn( state );
OmegaState.omega.handle( context, message, outgoing );
Mockito.verify( context ).roundDone( 1 );
assertEquals( 1, state.getFreshness() );
}
@Test
public void testRoundTripTimeoutAkaAdvanceEpoch() throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Message.internal( OmegaMessage.round_trip_timeout );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
State state = new State( new EpochNumber() );
Mockito.when( context.getMyState() ).thenReturn( state );
View myView = new View( state );
Mockito.when( context.getMyView() ).thenReturn( myView );
OmegaState result = (OmegaState) OmegaState.omega.handle( context, message, outgoing );
assertEquals( OmegaState.omega, result );
Mockito.verify( context ).getMyState();
Mockito.verify( context ).getMyView();
Mockito.verify( context, Mockito.never() ).roundDone( Matchers.anyInt() );
assertTrue( myView.isExpired() );
// Most important things to test - no update on freshness and serial num incremented
assertEquals( 1, state.getEpochNum().getSerialNum() );
assertEquals( 0, state.getFreshness() );
}
private static final String fromString = "neo4j://from";
private void testRefreshResponseOnState( boolean newer ) throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Mockito.mock( Message.class );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
// Value here is not important, we override the compareTo() method anyway
RefreshPayload payload = new RefreshPayload( 1, 1, 1, 1 );
Mockito.when( message.getHeader( Message.FROM ) ).thenReturn( fromString );
Mockito.when( message.getPayload() ).thenReturn( payload );
Mockito.when( message.getMessageType() ).thenReturn( OmegaMessage.refresh );
URI fromURI = new URI( fromString );
Map<URI, State> registry = Mockito.mock( Map.class );
State fromState = Mockito.mock( State.class );
Mockito.when( registry.get( fromURI ) ).thenReturn( fromState );
Mockito.when( context.getRegistry() ).thenReturn( registry );
if ( newer )
{
Mockito.when( fromState.compareTo( Matchers.any( State.class ) ) ).thenReturn( -1 );
}
else
{
Mockito.when( fromState.compareTo( Matchers.any( State.class ) ) ).thenReturn( 1 );
}
OmegaState.omega.handle( context, message, outgoing );
Mockito.verify( context, Mockito.atLeastOnce() ).getRegistry();
Mockito.verify( registry ).get( fromURI );
Mockito.verify( fromState ).compareTo( Matchers.isA( State.class ) ); // existing compared to the one from the message
if ( newer )
{
Mockito.verify( registry ).put( Matchers.eq( fromURI ), Matchers.isA( State.class ) );
}
else
{
Mockito.verify( registry, Mockito.never() ).put( Matchers.eq( fromURI ), Matchers.isA( State.class ) );
}
Mockito.verify( outgoing ).process( Matchers.argThat( new MessageArgumentMatcher<OmegaMessage>().to( fromURI
).onMessageType(
OmegaMessage.refresh_ack ) ) );
}
@Test
public void testRefreshResponseOnOlderState() throws Throwable
{
testRefreshResponseOnState( false );
}
@Test
public void testRefreshResponseOnNewerState() throws Throwable
{
testRefreshResponseOnState( true );
}
@Test
public void testCollectRoundStartsOnReadTimeout() throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Mockito.mock( Message.class );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
Set<URI> servers = new HashSet<URI>();
servers.add( new URI( "localhost:80" ) );
servers.add( new URI( "localhost:81" ) );
servers.add( new URI( "localhost:82" ) );
Mockito.when( context.getServers() ).thenReturn( (Collection) servers );
Mockito.when( message.getMessageType() ).thenReturn( OmegaMessage.read_timeout );
Mockito.when( context.getMyProcessId() ).thenReturn( 1 );
OmegaState.omega.handle( context, message, outgoing );
Mockito.verify( context, Mockito.atLeastOnce() ).getServers();
Mockito.verify( context ).startCollectionRound();
for ( URI server : servers )
{
Mockito.verify( outgoing ).process( Matchers.argThat( new MessageArgumentMatcher<OmegaMessage>().to(
server )
.onMessageType( OmegaMessage.collect ).withPayload( new CollectPayload( 0 ) ) ) );
}
}
@Test
public void testResponseOnCollectRequest() throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Mockito.mock( Message.class );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
Map<URI, State> dummyState = new HashMap<URI, State>();
Mockito.when( context.getRegistry() ).thenReturn( dummyState );
Mockito.when( message.getHeader( Message.FROM ) ).thenReturn( fromString );
Mockito.when( message.getPayload() ).thenReturn( new CollectPayload( 1 ) );
Mockito.when( message.getMessageType() ).thenReturn( OmegaMessage.collect );
OmegaState.omega.handle( context, message, outgoing );
Mockito.verify( context ).getRegistry();
Mockito.verify( outgoing ).process( Matchers.argThat( new MessageArgumentMatcher<OmegaMessage>().to( new URI(
fromString ) )
.onMessageType( OmegaMessage.status ).withPayload( new CollectResponsePayload( new URI[]{}, new RefreshPayload[]{}, 1 ) ) ));
}
private void testStatusResponseHandling( boolean done ) throws Throwable
{
OmegaContext context = Mockito.mock( OmegaContext.class );
Message<OmegaMessage> message = Mockito.mock( Message.class );
MessageProcessor outgoing = Mockito.mock( MessageProcessor.class );
URI fromUri = new URI( fromString );
Map<URI, State> thePayloadContents = new HashMap<URI, State>();
thePayloadContents.put( fromUri, new State( new EpochNumber( 1, 1 ), 1 ) );
CollectResponsePayload thePayload = CollectResponsePayload.fromRegistry( thePayloadContents, 3 /*== readNum*/ );
Mockito.when( message.getHeader( Message.FROM ) ).thenReturn( fromString );
Mockito.when( message.getPayload() ).thenReturn( thePayload );
Mockito.when( message.getMessageType() ).thenReturn( OmegaMessage.status );
Mockito.when( context.getViews() ).thenReturn( new HashMap<URI, View>() );
Mockito.when( context.getStatusResponsesForRound( 3 ) ).thenReturn( done ? 3 : 1 ); // less than half, not done
Mockito.when( context.getClusterNodeCount() ).thenReturn( 5 );
OmegaState.omega.handle( context, message, outgoing );
Mockito.verify( context ).responseReceivedForRound( 3, fromUri, thePayloadContents );
Mockito.verify( context ).getStatusResponsesForRound( 3 /*== readNum*/ );
Mockito.verify( context ).getClusterNodeCount();
if ( done )
{
Mockito.verify( context ).collectionRoundDone( 3 );
}
Mockito.verifyNoMoreInteractions( context );
// Receiving status response sends no messages anywhere, just alters context state
Mockito.verifyZeroInteractions( outgoing );
}
@Test
public void testStatusResponseHandlingRoundNotDone() throws Throwable
{
testStatusResponseHandling( false );
}
@Test
public void testStatusResponseHandlingRoundDone() throws Throwable
{
testStatusResponseHandling( true );
}
}