/**
* 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.client;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.neo4j.cluster.BindingListener;
import org.neo4j.cluster.ProtocolServer;
import org.neo4j.cluster.protocol.cluster.Cluster;
import org.neo4j.cluster.protocol.cluster.ClusterConfiguration;
import org.neo4j.cluster.protocol.cluster.ClusterListener;
import org.neo4j.kernel.impl.util.StringLogger;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;
import org.neo4j.kernel.logging.Loggers;
import org.neo4j.kernel.logging.Logging;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* This service starts quite late, and is available for the instance to join as a member in the cluster.
* <p/>
* It can either use manual listing of hosts, or auto discovery protocols.
*/
public class ClusterJoin
extends LifecycleAdapter
{
public interface Configuration
{
boolean isDiscoveryEnabled();
String[] getInitialHosts();
String getDiscoveryUrl();
String getClusterName();
}
private Configuration config;
private ProtocolServer protocolServer;
private StringLogger logger;
private URI clustersUri;
private Clusters clusters;
private Cluster cluster;
private URI serverId;
private DocumentBuilder builder;
private Transformer transformer;
public ClusterJoin( Configuration config, ProtocolServer protocolServer, Logging logger )
{
this.config = config;
this.protocolServer = protocolServer;
this.logger = logger.getLogger( Loggers.CLUSTER );
}
@Override
public void init() throws Throwable
{
cluster = protocolServer.newClient( Cluster.class );
builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
transformer = TransformerFactory.newInstance().newTransformer();
}
@Override
public void start() throws Throwable
{
acquireServerId();
// Now that we have our own id, do cluster join
if ( config.isDiscoveryEnabled() )
{
clusterDiscovery();
}
else
{
clusterByConfig();
}
}
@Override
public void stop()
{
final Semaphore semaphore = new Semaphore( 0 );
cluster.addClusterListener( new ClusterListener.Adapter()
{
@Override
public void leftCluster()
{
cluster.removeClusterListener( this );
semaphore.release();
}
} );
cluster.leave();
try
{
if ( !semaphore.tryAcquire( 5, TimeUnit.SECONDS ) )
{
logger.logMessage( "Unable to leave cluster, timeout" );
}
}
catch ( InterruptedException e )
{
Thread.interrupted();
logger.logMessage( "Unable to leave cluster, interrupted", e );
}
}
private void acquireServerId() throws RuntimeException
{
final Semaphore semaphore = new Semaphore( 0 );
protocolServer.addBindingListener( new BindingListener()
{
@Override
public void listeningAt( URI me )
{
serverId = me;
semaphore.release();
protocolServer.removeBindingListener( this );
}
} );
try
{
if ( !semaphore.tryAcquire( 1, TimeUnit.MINUTES ) )
{
throw new RuntimeException( "Unable to acquire server id, timed out" );
}
}
catch ( InterruptedException e )
{
Thread.interrupted();
throw new RuntimeException( "Unable to acquire server id, interrupted", e );
}
}
private void clusterDiscovery() throws URISyntaxException, ParserConfigurationException, SAXException, IOException
{
determineUri();
readClustersXml();
// Now try to join or create cluster
if ( clusters != null )
{
Clusters.Cluster clusterConfig = clusters.getCluster( config.getClusterName() );
if ( clusterConfig != null )
{
for ( Clusters.Member member : clusterConfig.getMembers() )
{
URI joinUri = new URI( "cluster://" + member.getHost() );
if ( !joinUri.equals( serverId ) )
{
Future<ClusterConfiguration> config = cluster.join( joinUri );
try
{
logger.logMessage( "Joined cluster:" + config.get() );
try
{
updateMyInfo();
}
catch ( TransformerException e )
{
throw new RuntimeException( e );
}
return;
}
catch ( InterruptedException e )
{
e.printStackTrace();
}
catch ( ExecutionException e )
{
logger.logMessage( "Could not join cluster member " + member.getHost() );
}
}
}
}
// Could not find cluster or not join nodes in cluster - create it!
if ( clusterConfig == null )
{
clusterConfig = new Clusters.Cluster( config.getClusterName() );
clusters.getClusters().add( clusterConfig );
}
cluster.create( clusterConfig.getName() );
if ( clusterConfig.getByUri( serverId ) == null )
{
clusterConfig.getMembers().add( new Clusters.Member( serverId.toString() ) );
try
{
updateMyInfo();
}
catch ( TransformerException e )
{
logger.logMessage( "Could not update cluster discovery file:" + clustersUri, e );
}
}
}
}
private void determineUri() throws URISyntaxException
{
String clusterUrlString = config.getDiscoveryUrl();
if ( clusterUrlString != null )
{
if ( clusterUrlString.startsWith( "/" ) )
{
clustersUri = Thread.currentThread().getContextClassLoader().getResource( clusterUrlString ).toURI();
}
else
{
clustersUri = new URI( clusterUrlString );
}
}
else
{
URL classpathURL = Thread.currentThread().getContextClassLoader().getResource( "clusters.xml" );
if ( classpathURL != null )
{
clustersUri = classpathURL.toURI();
}
}
}
private void readClustersXml() throws SAXException, IOException
{
if ( clustersUri.getScheme().equals( "file" ) )
{
File file = new File( clustersUri );
if ( file.exists() )
{
Document doc = builder.parse( file );
clusters = new ClustersXMLSerializer( builder ).read( doc );
clusters.setTimestamp( file.lastModified() );
}
}
}
private void updateMyInfo() throws TransformerException, IOException, SAXException
{
Clusters.Cluster cluster = clusters.getCluster( config.getClusterName() );
if ( cluster == null )
{
clusters.getClusters().add( cluster = new Clusters.Cluster( config.getClusterName() ) );
}
if ( cluster.contains( serverId ) )
{
// Do nothing
}
else
{
// Add myself to list
cluster.getMembers().add( new Clusters.Member( serverId.getHost() + (serverId.getPort() == -1 ? "" : ":" +
serverId.getPort()) ) );
Document document = new ClustersXMLSerializer( builder ).write( clusters );
// Save new version
if ( clustersUri.getScheme().equals( "file" ) )
{
File clustersFile = new File( clustersUri );
if ( clustersFile.lastModified() != clusters.getTimestamp() )
{
readClustersXml(); // Re-read XML file
updateMyInfo(); // Try again
return;
}
// Save new version
transformer.transform( new DOMSource( document ), new StreamResult( clustersFile ) );
clusters.setTimestamp( clustersFile.lastModified() );
}
else
{
// TODO Implement HTTP version
}
}
}
private void clusterByConfig()
{
String[] hosts = config.getInitialHosts();
if ( hosts.length == 0 )
{
logger.logMessage( "Creating cluster " + config.getClusterName() );
cluster.create( config.getClusterName() );
}
else
{
try
{
for ( String host : hosts )
{
if ( serverId.toString().endsWith( host ) )
{
continue; // Don't try to join myself
}
logger.info( "Attempting to join " + host );
Future<ClusterConfiguration> clusterConfig = cluster.join( new URI( "cluster://" + host ) );
try
{
logger.info( "Joined cluster:" + clusterConfig.get() );
return;
}
catch ( InterruptedException e )
{
e.printStackTrace();
}
catch ( ExecutionException e )
{
logger.error( "Could not join cluster member " + host );
}
}
// Failed to join cluster, create new one
cluster.create( config.getClusterName() );
}
catch ( URISyntaxException e )
{
// This
e.printStackTrace();
}
}
}
}