Skip to content

Commit

Permalink
Make the NeoServerRestartTest deterministic
Browse files Browse the repository at this point in the history
As well as addressing review comments.
  • Loading branch information
ragadeeshu committed Jan 5, 2017
1 parent 192fef6 commit 86216ba
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 83 deletions.
Expand Up @@ -25,16 +25,10 @@
import org.junit.Test; import org.junit.Test;


import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.OpenOption; import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;


import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.graphdb.mockfs.EphemeralFileSystemAbstraction;
import org.neo4j.helpers.collection.MapUtil; import org.neo4j.helpers.collection.MapUtil;
import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.io.fs.FileSystemAbstraction;
import org.neo4j.io.pagecache.PageCache; import org.neo4j.io.pagecache.PageCache;
Expand All @@ -46,7 +40,6 @@
import org.neo4j.logging.LogProvider; import org.neo4j.logging.LogProvider;
import org.neo4j.logging.NullLogProvider; import org.neo4j.logging.NullLogProvider;
import org.neo4j.test.rule.PageCacheRule; import org.neo4j.test.rule.PageCacheRule;
import org.neo4j.test.rule.fs.DefaultFileSystemRule;
import org.neo4j.test.rule.fs.EphemeralFileSystemRule; import org.neo4j.test.rule.fs.EphemeralFileSystemRule;


import static java.nio.file.StandardOpenOption.DELETE_ON_CLOSE; import static java.nio.file.StandardOpenOption.DELETE_ON_CLOSE;
Expand Down Expand Up @@ -175,7 +168,6 @@ public void shouldCompleteInitializationOfStoresWithIncompleteHeaders() throws E
StoreFactory storeFactory = storeFactory( Config.empty() ); StoreFactory storeFactory = storeFactory( Config.empty() );
storeFactory.openAllNeoStores( true ).close(); storeFactory.openAllNeoStores( true ).close();
FileSystemAbstraction fs = fsRule.get(); FileSystemAbstraction fs = fsRule.get();
File[] files = fs.listFiles( storeDir, ( file, s ) -> !s.endsWith( ".id" ) );
for ( File f : fs.listFiles( storeDir ) ) for ( File f : fs.listFiles( storeDir ) )
{ {
fs.truncate( f, 0 ); fs.truncate( f, 0 );
Expand Down
106 changes: 57 additions & 49 deletions community/server/src/main/java/org/neo4j/server/AbstractNeoServer.java
Expand Up @@ -103,7 +103,7 @@ public abstract class AbstractNeoServer implements NeoServer
private static final long MINIMUM_TIMEOUT = 1000L; private static final long MINIMUM_TIMEOUT = 1000L;
/** /**
* We add a second to the timeout if the user configures a 1-second timeout. * We add a second to the timeout if the user configures a 1-second timeout.
* * <p>
* This ensures the expiry time displayed to the user is always at least 1 second, even after it is rounded down. * This ensures the expiry time displayed to the user is always at least 1 second, even after it is rounded down.
*/ */
private static final long ROUNDING_SECOND = 1000L; private static final long ROUNDING_SECOND = 1000L;
Expand Down Expand Up @@ -153,16 +153,18 @@ public AbstractNeoServer( Config config, Database.Factory dbFactory,
this.logProvider = logProvider; this.logProvider = logProvider;
this.log = logProvider.getLog( getClass() ); this.log = logProvider.getLog( getClass() );


HttpConnector httpConnector = ClientConnectorSettings.httpConnector( config, ClientConnectorSettings.HttpConnector.Encryption.NONE ) HttpConnector httpConnector =
.orElseThrow( () -> ClientConnectorSettings.httpConnector( config, ClientConnectorSettings.HttpConnector.Encryption.NONE )
new IllegalArgumentException( "An HTTP connector must be configured to run the server" ) ); .orElseThrow( () ->
new IllegalArgumentException(
"An HTTP connector must be configured to run the server" ) );
httpListenAddress = config.get( httpConnector.listen_address ); httpListenAddress = config.get( httpConnector.listen_address );
httpAdvertisedAddress = config.get( httpConnector.advertised_address ); httpAdvertisedAddress = config.get( httpConnector.advertised_address );


Optional<HttpConnector> httpsConnector = Optional<HttpConnector> httpsConnector =
ClientConnectorSettings.httpConnector( config, ClientConnectorSettings.HttpConnector.Encryption.TLS ); ClientConnectorSettings.httpConnector( config, ClientConnectorSettings.HttpConnector.Encryption.TLS );
httpsListenAddress = httpsConnector.map( (connector) -> config.get( connector.listen_address ) ); httpsListenAddress = httpsConnector.map( ( connector ) -> config.get( connector.listen_address ) );
httpsAdvertisedAddress = httpsConnector.map( (connector) -> config.get( connector.advertised_address ) ); httpsAdvertisedAddress = httpsConnector.map( ( connector ) -> config.get( connector.advertised_address ) );
} }


@Override @Override
Expand All @@ -173,7 +175,8 @@ public void init()
return; return;
} }


this.database = life.add( dependencyResolver.satisfyDependency(dbFactory.newDatabase( config, dependencies)) ); this.database =
life.add( dependencyResolver.satisfyDependency( dbFactory.newDatabase( config, dependencies ) ) );


this.authManagerSupplier = dependencyResolver.provideDependency( AuthManager.class ); this.authManagerSupplier = dependencyResolver.provideDependency( AuthManager.class );
this.userManagerSupplier = dependencyResolver.provideDependency( UserManagerSupplier.class ); this.userManagerSupplier = dependencyResolver.provideDependency( UserManagerSupplier.class );
Expand All @@ -187,6 +190,9 @@ public void init()
registerModule( moduleClass ); registerModule( moduleClass );
} }


serverComponents = new ServerComponentsLifecycleAdapter();
life.add( serverComponents );

this.initialized = true; this.initialized = true;
} }


Expand All @@ -196,41 +202,6 @@ public void start() throws ServerStartupException
init(); init();
try try
{ {
serverComponents = new LifecycleAdapter()
{
@Override
public void start()
throws Throwable
{
DiagnosticsManager diagnosticsManager = resolveDependency( DiagnosticsManager.class );
Log diagnosticsLog = diagnosticsManager.getTargetLog();
diagnosticsLog.info( "--- SERVER STARTED START ---" );
databaseActions = createDatabaseActions();

transactionFacade = createTransactionalActions();

cypherExecutor = new CypherExecutor( database, logProvider );

configureWebServer();

cypherExecutor.start();

startModules();

startWebServer();

diagnosticsLog.info( "--- SERVER STARTED END ---" );
}

@Override
public void stop()
throws Throwable
{
stopWebServer();
stopModules();
}
};
life.add( serverComponents );
life.start(); life.start();


} }
Expand Down Expand Up @@ -261,7 +232,7 @@ private TransactionFacade createTransactionalActions()
final Clock clock = Clocks.systemClock(); final Clock clock = Clocks.systemClock();


transactionRegistry = transactionRegistry =
new TransactionHandleRegistry( clock, timeoutMillis, logProvider ); new TransactionHandleRegistry( clock, timeoutMillis, logProvider );


// ensure that this is > 0 // ensure that this is > 0
long runEvery = round( timeoutMillis / 2.0 ); long runEvery = round( timeoutMillis / 2.0 );
Expand All @@ -275,7 +246,8 @@ private TransactionFacade createTransactionalActions()
return new TransactionFacade( return new TransactionFacade(
new TransitionalPeriodTransactionMessContainer( database.getGraph() ), new TransitionalPeriodTransactionMessContainer( database.getGraph() ),
dependencyResolver.resolveDependency( QueryExecutionEngine.class ), dependencyResolver.resolveDependency( QueryExecutionEngine.class ),
dependencyResolver.resolveDependency( GraphDatabaseQueryService.class ), transactionRegistry, logProvider dependencyResolver.resolveDependency( GraphDatabaseQueryService.class ), transactionRegistry,
logProvider
); );
} }


Expand Down Expand Up @@ -348,7 +320,8 @@ private void startWebServer() throws Exception
} }
} }


private void setUpHttpLogging() throws IOException { private void setUpHttpLogging() throws IOException
{
if ( !getConfig().get( http_logging_enabled ) ) if ( !getConfig().get( http_logging_enabled ) )
{ {
return; return;
Expand Down Expand Up @@ -392,15 +365,17 @@ protected Optional<KeyStoreInformation> createKeyStore()
//noinspection deprecation //noinspection deprecation
log.info( "No SSL certificate found, generating a self-signed certificate.." ); log.info( "No SSL certificate found, generating a self-signed certificate.." );
Certificates certFactory = new Certificates(); Certificates certFactory = new Certificates();
certFactory.createSelfSignedCertificate( certificatePath, privateKeyPath, httpListenAddress.getHostname() ); certFactory.createSelfSignedCertificate( certificatePath, privateKeyPath,
httpListenAddress.getHostname() );
} }


// Make sure both files were there, or were generated // Make sure both files were there, or were generated
if ( !certificatePath.exists() ) if ( !certificatePath.exists() )
{ {
throw new ServerStartupException( throw new ServerStartupException(
String.format( String.format(
"TLS private key found, but missing certificate at '%s'. Cannot start server without certificate.", "TLS private key found, but missing certificate at '%s'. Cannot start server " +
"without certificate.",


certificatePath ) ); certificatePath ) );
} }
Expand Down Expand Up @@ -435,7 +410,6 @@ protected Optional<KeyStoreInformation> createKeyStore()
public void stop() public void stop()
{ {
life.stop(); life.stop();
life.remove( serverComponents );
} }


private void stopWebServer() private void stopWebServer()
Expand Down Expand Up @@ -534,7 +508,7 @@ private boolean hasModule( Class<? extends ServerModule> clazz )
return false; return false;
} }


@SuppressWarnings("unchecked") @SuppressWarnings( "unchecked" )
private <T extends ServerModule> T getModule( Class<T> clazz ) private <T extends ServerModule> T getModule( Class<T> clazz )
{ {
for ( ServerModule sm : serverModules ) for ( ServerModule sm : serverModules )
Expand Down Expand Up @@ -562,4 +536,38 @@ public DependencyResolver get()
return db.getGraph().getDependencyResolver(); return db.getGraph().getDependencyResolver();
} }
} ); } );

private class ServerComponentsLifecycleAdapter extends LifecycleAdapter
{
@Override
public void start() throws Throwable
{
DiagnosticsManager diagnosticsManager = resolveDependency( DiagnosticsManager.class );
Log diagnosticsLog = diagnosticsManager.getTargetLog();
diagnosticsLog.info( "--- SERVER STARTED START ---" );
databaseActions = createDatabaseActions();

transactionFacade = createTransactionalActions();

cypherExecutor = new CypherExecutor( database, logProvider );

configureWebServer();

cypherExecutor.start();

startModules();

startWebServer();

diagnosticsLog.info( "--- SERVER STARTED END ---" );
}

@Override
public void stop()
throws Throwable
{
stopWebServer();
stopModules();
}
}
} }
Expand Up @@ -49,7 +49,7 @@ public abstract class ServerBootstrapper implements Bootstrapper
public static final int WEB_SERVER_STARTUP_ERROR_CODE = 1; public static final int WEB_SERVER_STARTUP_ERROR_CODE = 1;
public static final int GRAPH_DATABASE_STARTUP_ERROR_CODE = 2; public static final int GRAPH_DATABASE_STARTUP_ERROR_CODE = 2;


private NeoServer server; private volatile NeoServer server;
private Thread shutdownHook; private Thread shutdownHook;
private GraphDatabaseDependencies dependencies = GraphDatabaseDependencies.newDependencies(); private GraphDatabaseDependencies dependencies = GraphDatabaseDependencies.newDependencies();
// in case we have errors loading/validating the configuration log to stdout // in case we have errors loading/validating the configuration log to stdout
Expand Down
@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2002-2016 "Neo Technology," * Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com] * Network Engine for Objects in Lund AB [http://neotechnology.com]
* *
* This file is part of Neo4j. * This file is part of Neo4j.
Expand All @@ -21,48 +21,99 @@


import org.junit.Test; import org.junit.Test;


import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;


import org.neo4j.server.helpers.CommunityServerBuilder; import org.neo4j.io.pagecache.PageEvictionCallback;
import org.neo4j.io.pagecache.PageSwapper;
import org.neo4j.io.pagecache.impl.SingleFilePageSwapperFactory;
import org.neo4j.test.ThreadTestUtils; import org.neo4j.test.ThreadTestUtils;


import static org.junit.Assert.fail;

public abstract class NeoServerRestartTest public abstract class NeoServerRestartTest
{ {
public static final String CUSTOM_SWAPPER = "CustomSwapper";
private static Semaphore semaphore;

static
{
semaphore = new Semaphore( 0 );
}

/**
* This test makes sure that the database is able to start after having been stopped during initialization.
*
* In order to make sure that the server is stopped during startup we create a separate thread that calls stop.
* In order to make sure that this thread does not call stop before the startup procedure has started we use a
* custom implementation of a PageSwapperFactory, which communicates with the thread that calls stop. We do this
* via a static semaphore.
* @throws IOException
* @throws InterruptedException
*/


@Test @Test
public void shouldNotCorruptWhenImmediatelyStopped() throws IOException, InterruptedException public void shouldBeAbleToRestartWhenStoppedDuringStartup() throws IOException, InterruptedException
{ {
NeoServer server = getNeoServer(); // Make sure that the semaphore is in a clean state.
semaphore.drainPermits();
// Get a server that uses our custom swapper.
NeoServer server = getNeoServer( CUSTOM_SWAPPER );


CountDownLatch latch = new CountDownLatch( 1 ); AtomicBoolean failure = new AtomicBoolean();
ThreadTestUtils.fork( $waitThenStopServer( server, latch ) ); Thread serverStoppingThread = ThreadTestUtils.fork( stopServerAfterStartingHasStarted( server, failure ) );
server.start(); server.start();
//Wait for the server to stop. // Wait for the server to stop.
latch.await(); serverStoppingThread.join();

// Check if the server stopped successfully.
server = getNeoServer(); if ( failure.get() )
{
fail( "Server failed to stop." );
}
// Verify that we can start the server again.
server = getNeoServer( CUSTOM_SWAPPER );
server.start(); server.start();
server.stop(); server.stop();
} }


protected abstract NeoServer getNeoServer() throws IOException; protected abstract NeoServer getNeoServer( String customPageSwapperName ) throws IOException;


private Runnable $waitThenStopServer( NeoServer server, CountDownLatch latch ) private Runnable stopServerAfterStartingHasStarted( NeoServer server, AtomicBoolean failure )
{ {
return () -> { return () ->
{
try try
{ {
//Make sure that we have started starting the database. This value is empirically chosen and may need // Make sure that we have started the startup procedure before calling stop.
// to be revisited in the future. semaphore.acquire();
Thread.sleep( 200 );
server.stop(); server.stop();
latch.countDown();
} }
catch ( Exception e ) catch ( Exception e )
{ {
//This is ok, it is not what we are testing for. failure.set( true );
e.printStackTrace();
} }
}; };
} }

// This class is used to notify the test that the server has started its startup procedure.
public static class CustomSwapper extends SingleFilePageSwapperFactory
{
@Override
public String implementationName()
{
return CUSTOM_SWAPPER;
}

@Override
public PageSwapper createPageSwapper( File file, int filePageSize, PageEvictionCallback onEviction,
boolean createIfNotExist ) throws IOException
{
// This will be called early in the startup sequence. Notifies that we can call stop on the server.
semaphore.release();
return super.createPageSwapper( file, filePageSize, onEviction, createIfNotExist );
}
}
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2002-2016 "Neo Technology," * Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com] * Network Engine for Objects in Lund AB [http://neotechnology.com]
* *
* This file is part of Neo4j. * This file is part of Neo4j.
Expand All @@ -21,13 +21,15 @@


import java.io.IOException; import java.io.IOException;


import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.server.helpers.CommunityServerBuilder; import org.neo4j.server.helpers.CommunityServerBuilder;


public class NeoServerRestartTestCommunity extends NeoServerRestartTest public class NeoServerRestartTestCommunity extends NeoServerRestartTest
{ {
protected NeoServer getNeoServer() throws IOException protected NeoServer getNeoServer( String customPageSwapperName ) throws IOException
{ {
CommunityServerBuilder builder = CommunityServerBuilder.server(); CommunityServerBuilder builder = CommunityServerBuilder.server().withProperty( GraphDatabaseSettings
.pagecache_swapper.name(), customPageSwapperName );
return builder.build(); return builder.build();
} }
} }
@@ -0,0 +1 @@
org.neo4j.server.NeoServerRestartTest$CustomSwapper

0 comments on commit 86216ba

Please sign in to comment.