/**
* 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.assertFalse;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.mockito.Mockito;
import org.neo4j.cluster.protocol.cluster.ClusterContext;
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 OmegaContextTest
{
@Test
public void testOrderingOfEpochNumberOnSerialNum()
{
List<EpochNumber> toSort = new LinkedList<EpochNumber>();
// Creates a list in order 5,4,6,3,7,2,8,1,9
for ( int i = 1; i < 10; i++ )
{
// The sign code is lame, but i couldn't figure a branch free way
EpochNumber epoch = new EpochNumber( 5 + ((i + 1) / 2) * (i % 2 == 0 ? -1 : 1), i );
toSort.add( epoch );
}
Collections.sort( toSort );
for ( int i = 1; i < toSort.size(); i++ )
{
EpochNumber prev = toSort.get( i - 1 );
EpochNumber current = toSort.get( i );
assertTrue( prev.getSerialNum() < current.getSerialNum() );
}
}
@Test
public void testOrderingOfEpochNumberOnProcessId()
{
List<EpochNumber> toSort = new LinkedList<EpochNumber>();
// Creates a list in order 5,4,6,3,7,2,8,1,9
for ( int i = 1; i < 10; i++ )
{
EpochNumber epoch = new EpochNumber( 1, 5 + ((i + 1) / 2) * (i % 2 == 0 ? -1 : 1) );
toSort.add( epoch );
}
Collections.sort( toSort );
for ( int i = 1; i < toSort.size(); i++ )
{
EpochNumber prev = toSort.get( i - 1 );
EpochNumber current = toSort.get( i );
assertTrue( prev.getProcessId() < current.getProcessId() );
}
}
@Test
public void testOrderingEqualEpochs()
{
assertEquals( 0, new EpochNumber().compareTo( new EpochNumber() ) );
}
@Test
public void testOrderingOfStateOnEpochNum()
{
List<State> toSort = new LinkedList<State>();
// Creates a list in order 5,4,6,3,7,2,8,1,9
for ( int i = 1; i < 10; i++ )
{
EpochNumber epoch = new EpochNumber( 5 + ((i + 1) / 2) * (i % 2 == 0 ? -1 : 1), 1 );
State state = new State( epoch, 1 );
toSort.add( state );
}
Collections.sort( toSort );
for ( int i = 1; i < toSort.size(); i++ )
{
State prev = toSort.get( i - 1 );
State current = toSort.get( i );
assertTrue( prev.getEpochNum().getSerialNum() < current.getEpochNum().getSerialNum() );
}
}
@Test
public void testBasicRefreshRound()
{
OmegaContext context = new OmegaContext( Mockito.mock( ClusterContext.class ) );
assertEquals( -1, context.getAckCount( 0 ) );
int refreshRoundOne = context.startRefreshRound();
assertEquals( 0, context.getAckCount( refreshRoundOne ) );
context.ackReceived( refreshRoundOne );
assertEquals( 1, context.getAckCount( refreshRoundOne ) );
context.roundDone( refreshRoundOne );
assertEquals( -1, context.getAckCount( refreshRoundOne ) );
}
@Test
public void testTwoParallelRefreshRounds()
{
OmegaContext context = new OmegaContext( Mockito.mock( ClusterContext.class ) );
int refreshRoundOne = context.startRefreshRound();
context.ackReceived( refreshRoundOne );
int refreshRoundTwo = context.startRefreshRound();
context.ackReceived( refreshRoundOne );
context.ackReceived( refreshRoundTwo );
assertEquals( 2, context.getAckCount( refreshRoundOne ) );
assertEquals( 1, context.getAckCount( refreshRoundTwo ) );
context.roundDone( refreshRoundOne );
assertEquals( -1, context.getAckCount( refreshRoundOne ) );
assertEquals( 1, context.getAckCount( refreshRoundTwo ) );
}
@Test
public void testFirstAndSecondCollectionRound() throws URISyntaxException
{
OmegaContext context = new OmegaContext( Mockito.mock( ClusterContext.class ) );
int firstCollectionRound = context.startCollectionRound();
assertEquals( 1, firstCollectionRound );
assertEquals( Collections.emptyMap(), context.getPreviousViewForCollectionRound( firstCollectionRound ) );
assertEquals( 0, context.getStatusResponsesForRound( firstCollectionRound ) );
State state1 = new State( new EpochNumber(), 1 );
State state2 = new State( new EpochNumber(), 1 );
URI uri1 = new URI( "neo4j://server1" );
URI uri2 = new URI( "neo4j://server2" );
Map<URI, State> registry = new HashMap<URI, State>();
registry.put( uri1, state1 );
registry.put( uri2, state2 );
Map<URI, State> emptyView = Collections.emptyMap();
context.responseReceivedForRound( firstCollectionRound, uri1, emptyView );
// Really checking the invariants on the COLLECT phase of the algo is a test case on its own, below
assertEquals( 1, context.getStatusResponsesForRound( firstCollectionRound ) );
context.responseReceivedForRound( firstCollectionRound, uri2, emptyView );
assertEquals( 2, context.getStatusResponsesForRound( firstCollectionRound ) );
context.collectionRoundDone( firstCollectionRound );
int secondCollectionRound = context.startCollectionRound();
assertEquals( secondCollectionRound, firstCollectionRound + 1 );
assertEquals( context.getViews(), context.getPreviousViewForCollectionRound( secondCollectionRound ) );
}
@Test
public void testCollectInvariantsHoldAfterTwoCollectResponses() throws URISyntaxException
{
URI uri1 = new URI( "neo4j://server1" );
URI uri2 = new URI( "neo4j://server2" );
URI uri3 = new URI( "neo4j://server3" );
URI uri4 = new URI( "neo4j://server4" );
// Also, assume this is the first round
Map<URI, View> finalViews = new HashMap<URI, View>();
finalViews.put( uri1, new View( new State( new EpochNumber( 1 ), 3 ), false ) );
finalViews.put( uri2, new View( new State( new EpochNumber( 2 ), 3 ), false ) );
finalViews.put( uri3, new View( new State( new EpochNumber( 0 ), 5 ), false ) );
Map<URI, State> registryFrom1 = new HashMap<URI, State>();
registryFrom1.put( uri1, new State( new EpochNumber( 1 ), 3 ) );
registryFrom1.put( uri2, new State( new EpochNumber( 2 ), 3 ) );
registryFrom1.put( uri3, new State( new EpochNumber( 0 ), 5 ) );
Map<URI, State> registryFrom2 = new HashMap<URI, State>();
registryFrom2.put( uri1, new State( new EpochNumber( 1 ), 3 ) );
registryFrom2.put( uri2, new State( new EpochNumber( 1 ), 3 ) );
registryFrom2.put( uri3, new State( new EpochNumber( 0 ), 4 ) );
OmegaContext context = new OmegaContext( Mockito.mock( ClusterContext.class ) );
int collectionRound = context.startCollectionRound();
context.responseReceivedForRound( collectionRound, uri1, registryFrom1 );
checkConsolidatedViews( context.getViews(), registryFrom1 );
context.responseReceivedForRound( collectionRound, uri2, registryFrom2 );
checkConsolidatedViews( context.getViews(), registryFrom2 );
// Now we have collected responses from a majority. Check that the views have been expired properly
context.collectionRoundDone( collectionRound );
// We know the rest of the servers provide a maximum of 3 instances - our views must be at least as large
checkUpdatedViews( context.getPreviousViewForCollectionRound( collectionRound ), context.getViews() );
assertEquals( finalViews, context.getViews() );
// Time for the second round. This is the expected
finalViews.put( uri1, new View( new State( new EpochNumber( 1 ), 3 ) ) );
finalViews.put( uri2, new View( new State( new EpochNumber( 2 ), 3 ) ) );
finalViews.put( uri3, new View( new State( new EpochNumber( 4 ), 10 ), false ) );
finalViews.put( uri4, new View( new State( new EpochNumber( 1 ), 2 ), false ) );
registryFrom1.put( uri1, new State( new EpochNumber( 1 ), 3 ) );
registryFrom1.put( uri2, new State( new EpochNumber( 1 ), 3 ) );
registryFrom1.put( uri3, new State( new EpochNumber( 4 ), 10 ) );
registryFrom2.put( uri1, new State( new EpochNumber( 1 ), 3 ) );
registryFrom2.put( uri2, new State( new EpochNumber( 1 ), 3 ) );
registryFrom2.put( uri3, new State( new EpochNumber( 3 ), 9 ) );
// and all of the sudden, we have seen a new instance
registryFrom2.put( uri4, new State( new EpochNumber( 1 ), 2 ) );
collectionRound = context.startCollectionRound();
context.responseReceivedForRound( collectionRound, uri1, registryFrom1 );
checkConsolidatedViews( context.getViews(), registryFrom1 );
context.responseReceivedForRound( collectionRound, uri2, registryFrom2 );
checkConsolidatedViews( context.getViews(), registryFrom2 );
// Now we have collected responses from a majority. Check that the views have been expired properly
context.collectionRoundDone( collectionRound );
// We know the rest of the servers provide a maximum of 3 instances - our views must be at least as large
checkUpdatedViews( context.getPreviousViewForCollectionRound( collectionRound ), context.getViews() );
assertEquals( finalViews, context.getViews() );
}
private void checkConsolidatedViews( Map<URI, View> collectorViews, Map<URI, State> collectedRegistry )
{
for ( Map.Entry<URI, View> view : collectorViews.entrySet() )
{
URI uri = view.getKey();
State viewedState = view.getValue().getState();
assertTrue( viewedState.compareTo( collectedRegistry.get( uri ) ) >= 0 );
}
}
private void checkUpdatedViews( Map<URI, View> oldViews, Map<URI, View> newViews )
{
for ( Map.Entry<URI, View> view : newViews.entrySet() )
{
URI uri = view.getKey();
View newView = view.getValue();
View oldView = oldViews.get( uri );
if ( oldView == null )
{
assertFalse( newView.isExpired() );
continue;
}
if ( newView.getState().compareTo( oldView.getState() ) <= 0 )
{
assertTrue( newView.isExpired() );
}
if ( newView.getState().getEpochNum().compareTo( oldView.getState().getEpochNum() ) > 0 )
{
assertFalse( newView.isExpired() );
}
}
}
}