/** * 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.test.server.ha; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintStream; import java.lang.reflect.Array; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import org.neo4j.cluster.ClusterSettings; import org.neo4j.helpers.Pair; import org.neo4j.helpers.Triplet; import org.neo4j.jmx.impl.JmxKernelExtension; import org.neo4j.kernel.GraphDatabaseAPI; import org.neo4j.kernel.ha.HaSettings; import org.neo4j.kernel.ha.cluster.ClusterMemberState; import org.neo4j.management.HighAvailability; import org.neo4j.server.Bootstrapper; import org.neo4j.server.configuration.Configurator; import org.neo4j.server.enterprise.EnterpriseBootstrapper; import org.neo4j.test.TargetDirectory; import org.neo4j.test.subprocess.SubProcess; public final class ServerCluster { private static final Random random = new Random(); private final Triplet<ServerManager, URI, File>[] servers; public ServerCluster( String testName, TargetDirectory targetDir, Pair<Integer, Integer>... ports ) { // @SuppressWarnings( { "unchecked", "hiding" } ) // Pair<ServerManager, File>[] servers = new Pair[ports.length]; this.servers = new Triplet[ports.length]; SubProcess<ServerManager, String> process = new ServerProcess( testName ); try { for ( int i = 0; i < ports.length; i++ ) { Pair<String, File> config = config( testName, targetDir, i, ports[i] ); servers[i] = awaitStartup( Pair.of( process.start( config.first() ), config.other() ) )[0]; } } catch ( Throwable e ) { // shutdownAndCleanUp( servers ); for ( Pair<URI, File> UGLY_CODE_DUE_TO_ISSUES_IN_HA : (Pair<URI, File>[]) shutdown( (Class) Pair.class, servers ) ) { if( UGLY_CODE_DUE_TO_ISSUES_IN_HA != null ) { TargetDirectory.recursiveDelete( UGLY_CODE_DUE_TO_ISSUES_IN_HA.other() ); } } if( e instanceof Error ) { throw (Error) e; } if( e instanceof RuntimeException ) { throw (RuntimeException) e; } throw new RuntimeException( "Cluster startup failed", e ); } // this.servers = awaitStartup( servers ); System.out.println( "Started " + this ); } @Override public String toString() { StringBuilder result = new StringBuilder( "ServerCluster[" ); String prefix = ""; for ( Triplet<ServerManager, URI, File> server : servers ) { if ( server == null ) { result.append( "--" ); } else { result.append( prefix ).append( server.first() ).append( ": " ).append( server.second() ); if( server.first().isMaster() ) { result.append( " is master" ); } } prefix = ", "; } return result.append( "]" ).toString(); } public URI getRandomServerUri( URI... exclude ) { Set<URI> excluded; if ( exclude == null || exclude.length == 0 ) { excluded = Collections.emptySet(); } else { excluded = new HashSet<URI>( Arrays.asList( exclude ) ); } List<URI> candidates = new ArrayList<URI>(); for ( Triplet<ServerManager, URI, File> server : servers ) { if( server != null && !excluded.contains( server.second() ) ) { candidates.add( server.second() ); } } if( candidates.isEmpty() ) { throw new IllegalStateException( "No servers available" ); } return candidates.get( random.nextInt( candidates.size() ) ); } public void kill( URI server ) { for ( int i = 0; i < servers.length; i++ ) { if ( server.equals( servers[i].second() ) ) { SubProcess.kill( servers[i].first() ); servers[i] = null; return; } } throw new IllegalArgumentException( "No such server: " + server ); } public void updateAll() { for ( Triplet<ServerManager, URI, File> server : servers ) { server.first().update(); } } public void shutdown() { shutdown( null, servers ); } @SuppressWarnings( "unchecked" ) private static Pair<String, File> config( String name, TargetDirectory targetDir, int id, Pair<Integer, Integer> ports ) { File serverDir = targetDir.directory( "server-" + ports.other(), true ); File serverConfig = new File( serverDir, "server.cfg" ); File dbConfig = new File( serverDir, "neo4j.cfg" ); File dbDir = new File( serverDir, "graph-database" ); // Server configuration config( serverConfig,// Pair.of( Configurator.DB_MODE_KEY, "ha" ),// Pair.of( Configurator.DATABASE_LOCATION_PROPERTY_KEY, dbDir.getAbsolutePath() ),// Pair.of( Configurator.WEBSERVER_PORT_PROPERTY_KEY, ports.other().toString() ),// Pair.of( Configurator.DB_TUNING_PROPERTY_FILE_KEY, dbConfig.getAbsolutePath() ) ); // Kernel (and HA) configuration config( dbConfig, // Pair.of( ClusterSettings.cluster_name.name(), name ),// Pair.of( ClusterSettings.ha_server.name(), "localhost:" + ports.first() ),// Pair.of( ClusterSettings.server_id.name(), Integer.toString( id ) ) ); return Pair.of( serverConfig.getAbsolutePath(), serverDir ); } private static void config( File configFile, Pair<String, String>... config ) { String[] content = new String[config.length]; for ( int i = 0; i < config.length; i++ ) { content[i] = config[i].first() + "=" + config[i].other(); } write( configFile, content ); } private static void write( File target, String... content ) { PrintStream writer; try { writer = new PrintStream( target ); } catch ( FileNotFoundException e ) { throw new RuntimeException( e ); } try { for ( String line : content ) { writer.println( line ); } } finally { writer.close(); } } private static void shutdownAndCleanUp( Pair<ServerManager, File>[] servers ) { for ( File store : shutdown( File.class, servers ) ) { if( store != null ) { TargetDirectory.recursiveDelete( store ); } } } private static <T> T[] shutdown( Class<T> type, Pair<ServerManager, T>[] servers ) { @SuppressWarnings( "unchecked" ) T[] result = ( type != null ) ? (T[]) Array.newInstance( type, servers.length ) : null; for ( int i = 0; i < servers.length; i++ ) { if ( servers[i] != null ) { SubProcess.stop( servers[i].first() ); if( result != null ) { result[ i ] = servers[ i ].other(); } } } return result; } private static Triplet<ServerManager, URI, File>[] awaitStartup( Pair<ServerManager, File>... managers ) { @SuppressWarnings( "unchecked" ) Triplet<ServerManager, URI, File>[] result = new Triplet[managers.length]; for ( int i = 0; i < result.length; i++ ) { Pair<ServerManager, File> manager = managers[i]; result[i] = Triplet.of( manager.first(), manager.first().awaitStartup(), manager.other() ); } return result; } public interface ServerManager { URI awaitStartup(); void update(); boolean isMaster(); } private static class ServerProcess extends SubProcess<ServerManager, String> implements ServerManager { private transient Bootstrapper bootstrap = null; private transient volatile Integer startupStatus = null; private final String name; ServerProcess( String name ) { this.name = name; } @Override public String toString() { return name; } @Override protected void startup( String configFilePath ) { System.out.println( "configFilePath=" + configFilePath ); System.setProperty( Configurator.NEO_SERVER_CONFIG_FILE_KEY, configFilePath ); this.bootstrap = new EnterpriseBootstrapper(); this.startupStatus = this.bootstrap.start(); } @Override protected void shutdown( boolean normal ) { if( this.bootstrap != null ) { this.bootstrap.stop(); } super.shutdown( normal ); } private HighAvailability ha() { return graphDb().getDependencyResolver().resolveDependency( JmxKernelExtension.class ).getSingleManagementBean( HighAvailability.class ); } private GraphDatabaseAPI graphDb() { return this.bootstrap.getServer().getDatabase().getGraph(); } @Override public URI awaitStartup() { try { while( startupStatus == null ) { Thread.sleep( 10 ); } } catch ( InterruptedException ex ) { throw new RuntimeException( "Interrupted during startup", ex ); } if( startupStatus.equals( Bootstrapper.GRAPH_DATABASE_STARTUP_ERROR_CODE ) ) { throw new RuntimeException( "Database startup failure" ); } if( startupStatus.equals( Bootstrapper.WEB_SERVER_STARTUP_ERROR_CODE ) ) { throw new RuntimeException( "Server startup failure" ); } return bootstrap.getServer().baseUri(); } @Override public boolean isMaster() { return ClusterMemberState.MASTER.toString().equals( ha().getInstanceState() ); } @Override public void update() { System.out.println( ha().update() ); } } }