/*
* Copyright (c) 2002-2009 "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.onlinebackup;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import org.neo4j.kernel.EmbeddedGraphDatabase;
/**
* Online backup implementation for Neo4j.
*/
public class Neo4jBackup implements Backup
{
private final EmbeddedGraphDatabase onlineGraphDb;
private String destDir;
private List<String> xaNames = null;
private EmbeddedGraphDatabase backupGraphDb = null;
private static Logger logger =
Logger.getLogger( Neo4jBackup.class.getName() );
private static ConsoleHandler consoleHandler = new ConsoleHandler();
private static FileHandler fileHandler = null;
private static final Level LOG_LEVEL_NORMAL = Level.INFO;
private static final Level LOG_LEVEL_DEBUG = Level.ALL;
private static final Level LOG_LEVEL_OFF = Level.OFF;
static
{
logger.setUseParentHandlers( false );
logger.setLevel( LOG_LEVEL_NORMAL );
consoleHandler.setLevel( LOG_LEVEL_NORMAL );
logger.addHandler( consoleHandler );
}
/**
* Backup from a running {@link EmbeddedGraphDatabase} to a destination
* directory.
* @param sourceGraphDb
* running database as backup source
* @param destDir
* location of backup destination
*/
public Neo4jBackup( final EmbeddedGraphDatabase sourceGraphDb,
final String destDir )
{
if ( sourceGraphDb == null )
{
throw new IllegalArgumentException(
"The graph database instance is null." );
}
if ( destDir == null )
{
throw new IllegalArgumentException( "Destination dir is null." );
}
if ( !new File( destDir ).exists() )
{
throw new RuntimeException(
"Unable to locate local onlineGraphDb store in[" +
destDir + "]" );
}
this.onlineGraphDb = sourceGraphDb;
this.destDir = destDir;
}
/**
* Backup from a running {@link EmbeddedGraphDatabase} to another running
* {@link EmbeddedGraphDatabase}.
* @param sourceGraphDb
* running database as backup source
* @param destGraphDb
* running database as backup destination
*/
public Neo4jBackup( final EmbeddedGraphDatabase sourceGraphDb,
final EmbeddedGraphDatabase destGraphDb )
{
if ( sourceGraphDb == null )
{
throw new IllegalArgumentException(
"The online graph db instance is null." );
}
if ( destGraphDb == null )
{
throw new IllegalArgumentException(
"The backup destination graph db instance is null." );
}
this.onlineGraphDb = sourceGraphDb;
this.backupGraphDb = destGraphDb;
}
/**
* Backup from a running {@link EmbeddedGraphDatabase} to a destination
* directory including other data sources. NOTE: For now it assumes there is
* only a LuceneIndexService running besides Neo4j. Common data source names
* are "nioneodb" and "lucene".
* @param sourceGraphDb
* running database as backup source
* @param destDir
* location of backup destination
* @param xaDataSourceNames
* names of data sources to backup
*/
public Neo4jBackup( final EmbeddedGraphDatabase sourceGraphDb,
final String destDir, final List<String> xaDataSourceNames )
{
this( sourceGraphDb, destDir );
initXaNames( xaDataSourceNames );
}
/**
* Backup from a running {@link EmbeddedGraphDatabase} to another running
* {@link EmbeddedGraphDatabase} including other data sources. Common data
* source names are "nioneodb" and "lucene".
* @param sourceGraphDb
* running database as backup source
* @param destGraphDb
* running database as backup destination
* @param xaDataSourceNames
* names of data sources to backup
*/
public Neo4jBackup( final EmbeddedGraphDatabase sourceGraphDb,
final EmbeddedGraphDatabase destGraphDb,
final List<String> xaDataSourceNames )
{
this( sourceGraphDb, destGraphDb );
initXaNames( xaDataSourceNames );
}
/**
* @param xaDataSourceNames
*/
private final void initXaNames( final List<String> xaDataSourceNames )
{
if ( xaDataSourceNames == null )
{
throw new IllegalArgumentException( "xaDataSourceNames is null." );
}
if ( xaDataSourceNames.size() < 1 )
{
throw new IllegalArgumentException(
"xaDataSourceNames list is empty." );
}
this.xaNames = xaDataSourceNames;
}
public void doBackup() throws IOException
{
logger.info( "Initializing backup." );
Neo4jResource srcResource =
new EmbeddedGraphDatabaseResource( onlineGraphDb );
if ( xaNames == null )
{
if ( backupGraphDb == null )
{
Neo4jResource dstResource = LocalGraphDatabaseResource
.getInstance( destDir );
runSimpleBackup( srcResource, dstResource );
dstResource.close();
}
else
{
Neo4jResource dstResource =
new EmbeddedGraphDatabaseResource( backupGraphDb );
runSimpleBackup( srcResource, dstResource );
}
}
else
{
if ( backupGraphDb == null )
{
// TODO this is a temporary fix until we can restore services
Neo4jResource dstResource = LocalLuceneIndexResource
.getInstance( destDir );
runMultiBackup( srcResource, dstResource );
dstResource.close();
}
else
{
Neo4jResource dstResource =
new EmbeddedGraphDatabaseResource( backupGraphDb );
runMultiBackup( srcResource, dstResource );
}
}
}
/**
* Backup Neo4j data source only.
* @param srcResource
* backup source
* @param dstResource
* backup destination
* @throws IOException
*/
private void runSimpleBackup( final Neo4jResource srcResource,
final Neo4jResource dstResource ) throws IOException
{
Neo4jBackupTask task = new Neo4jBackupTask( srcResource.getDataSource(),
dstResource.getDataSource() );
task.prepare();
task.run();
logger.info( "Completed backup of [" + srcResource.getName()
+ "] data source." );
}
/**
* Backup multiple data sources.
* @param srcResource
* backup source
* @param dstResource
* backup destination
* @throws IOException
*/
private void runMultiBackup( final Neo4jResource srcResource,
final Neo4jResource dstResource ) throws IOException
{
List<Neo4jBackupTask> tasks = new ArrayList<Neo4jBackupTask>();
logger.info( "Checking and preparing " + xaNames.toString()
+ " data sources." );
for ( String xaName : xaNames )
{
// check source
XaDataSourceResource srcDataSource = srcResource
.getDataSource( xaName );
if ( srcDataSource == null )
{
String message = "XaDataSource not found in backup source: ["
+ xaName + "]";
logger.severe( message );
throw new RuntimeException( message );
}
else
{
// check destination
XaDataSourceResource dstDataSource = dstResource
.getDataSource( xaName );
if ( dstDataSource == null )
{
String message = "XaDataSource not found in backup destination: ["
+ xaName + "]";
logger.severe( message );
throw new RuntimeException( message );
}
else
{
Neo4jBackupTask task = new Neo4jBackupTask( srcDataSource,
dstDataSource );
task.prepare();
tasks.add( task );
}
}
}
if ( tasks.size() == 0 )
{
String message = "No data sources to backup were found.";
logger.severe( message );
throw new RuntimeException( message );
}
else
{
for ( Neo4jBackupTask task : tasks )
{
task.run();
}
logger.info( "Completed backup of " + tasks + " data sources." );
}
}
/**
* Class to handle backup tasks. It separates preparing and running the
* backup.
*/
private class Neo4jBackupTask
{
private final XaDataSourceResource src;
private final XaDataSourceResource dst;
private long srcVersion = -1;
private long dstVersion = -1;
private final String resourceName;
/**
* Create a backup task.
* @param src
* wrapped data source for source
* @param dst
* wrapped data source for destination
* @param resourceName
* name of data source
*/
private Neo4jBackupTask( final XaDataSourceResource src,
final XaDataSourceResource dst )
{
this.src = src;
this.dst = dst;
this.resourceName = src.getName();
}
/**
* Rotate log and check versions.
* @throws IOException
*/
public void prepare() throws IOException
{
logger.fine( "Checking and preparing data source: [" + resourceName
+ "]" );
// check store identities
if ( src.getCreationTime() != dst.getCreationTime()
&& src.getIdentifier() != dst.getIdentifier() )
{
String message = "Source[" + src.getCreationTime() + ","
+ src.getIdentifier() + "] is not same as destination["
+ dst.getCreationTime() + "," + dst.getIdentifier()
+ "] for resource [" + resourceName + "]";
logger.severe( message );
throw new RuntimeException( message );
}
// check versions
srcVersion = src.getVersion();
dstVersion = dst.getVersion();
if ( srcVersion < dstVersion )
{
String message = "Source srcVersion[" + srcVersion
+ "] < destination srcVersion[" + dstVersion
+ "] for resource [" + resourceName + "]";
logger.severe( message );
throw new RuntimeException( message );
}
// rotate log, check versions
src.rotateLog();
srcVersion = src.getVersion();
if ( srcVersion < dstVersion )
{
final String message = "Source srcVersion[" + srcVersion
+ "] < destination srcVersion[" + dstVersion
+ "] after rotate for resource [" + resourceName + "]";
logger.severe( message );
throw new RuntimeException( message );
}
// check that log entries exist
for ( long i = dstVersion; i < srcVersion; i++ )
{
if ( !src.hasLogicalLog( i ) )
{
String message = "Missing log entry in backup source: ["
+ i + "] in resource [" + resourceName
+ "]. Can not perform backup.";
logger.severe( message );
throw new RuntimeException( message );
}
}
// setup destination as slave
dst.makeBackupSlave();
}
/**
* Run the backup.
* @throws IOException
*/
public void run() throws IOException
{
if ( srcVersion == -1 || dstVersion == -1 )
{
final String message = "Backup can not start: source and/or destination "
+ "could not be prepared for backup: ["
+ resourceName
+ "]";
logger.severe( message );
throw new RuntimeException( message );
}
logger.fine( "Backing up data source: [" + resourceName + "]" );
for ( long i = dstVersion; i < srcVersion; i++ )
{
logger.fine( "Applying logical log [" + i + "] on ["
+ resourceName + "]" );
dst.applyLog( src.getLogicalLog( i ) );
}
logger.fine( "Source and destination have been synchronized. "
+ "Backup of data source complete [" + dstVersion + "->"
+ srcVersion + "] on [" + resourceName + "]." );
}
/**
* Returns the resource name for this task.
*/
@Override
public String toString()
{
return resourceName;
}
}
public void enableFileLogger() throws SecurityException, IOException
{
if ( fileHandler == null )
{
// create appending file logger
fileHandler = new FileHandler( "backup.log", true );
fileHandler.setLevel( consoleHandler.getLevel() );
fileHandler.setFormatter( new SimpleFormatter() );
logger.addHandler( fileHandler );
}
}
public void disableFileLogger()
{
if ( fileHandler != null )
{
logger.removeHandler( fileHandler );
fileHandler = null;
}
}
public void setLogLevelDebug()
{
logger.setLevel( LOG_LEVEL_DEBUG );
consoleHandler.setLevel( LOG_LEVEL_DEBUG );
if ( fileHandler != null )
{
fileHandler.setLevel( LOG_LEVEL_DEBUG );
}
}
public void setLogLevelNormal()
{
logger.setLevel( LOG_LEVEL_NORMAL );
consoleHandler.setLevel( LOG_LEVEL_NORMAL );
if ( fileHandler != null )
{
fileHandler.setLevel( LOG_LEVEL_NORMAL );
}
}
public void setLogLevelOff()
{
logger.setLevel( LOG_LEVEL_OFF );
}
}