/**
* 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.backup;
import static java.util.Collections.emptyMap;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.neo4j.com.RequestContext;
import org.neo4j.com.RequestContext.Tx;
import org.neo4j.com.Response;
import org.neo4j.com.ServerUtil;
import org.neo4j.com.ServerUtil.TxHandler;
import org.neo4j.com.StoreWriter;
import org.neo4j.com.ToFileStoreWriter;
import org.neo4j.com.TransactionStream;
import org.neo4j.com.TxExtractor;
import org.neo4j.consistency.ConsistencyCheckService;
import org.neo4j.consistency.checking.full.ConsistencyCheckIncompleteException;
import org.neo4j.graphdb.factory.GraphDatabaseSetting;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.helpers.ProgressIndicator;
import org.neo4j.helpers.Triplet;
import org.neo4j.helpers.progress.ProgressMonitorFactory;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.kernel.GraphDatabaseAPI;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.configuration.ConfigParam;
import org.neo4j.kernel.impl.nioneo.store.NeoStore;
import org.neo4j.kernel.impl.transaction.XaDataSourceManager;
import org.neo4j.kernel.impl.transaction.xaframework.LogIoUtils;
import org.neo4j.kernel.impl.transaction.xaframework.NoSuchLogVersionException;
import org.neo4j.kernel.impl.transaction.xaframework.XaDataSource;
import org.neo4j.kernel.impl.util.StringLogger;
class BackupService
{
class BackupOutcome
{
private final Map<String, Long> lastCommittedTxs;
BackupOutcome( Map<String, Long> lastCommittedTxs )
{
this.lastCommittedTxs = lastCommittedTxs;
}
public Map<String, Long> getLastCommittedTxs()
{
return Collections.unmodifiableMap( lastCommittedTxs );
}
}
BackupOutcome doFullBackup( String sourceHostNameOrIp, int sourcePort, String targetDirectory,
boolean checkConsistency, Config tuningConfiguration )
{
if ( directoryContainsDb( targetDirectory ) )
{
throw new RuntimeException( targetDirectory + " already contains a database" );
}
BackupClient client = new BackupClient( sourceHostNameOrIp, sourcePort, StringLogger.DEV_NULL, null );
client.start();
long timestamp = System.currentTimeMillis();
Map<String, Long> lastCommittedTxs = emptyMap();
try
{
Response<Void> response = client.fullBackup( decorateWithProgressIndicator(
new ToFileStoreWriter( new File( targetDirectory ) ) ) );
GraphDatabaseAPI targetDb = startTemporaryDb( targetDirectory,
VerificationLevel.NONE /* run full check instead */ );
try
{
// First, receive all txs pending
lastCommittedTxs = unpackResponse( response,
targetDb.getDependencyResolver().resolveDependency( XaDataSourceManager.class ),
ServerUtil.txHandlerForFullCopy() );
// Then go over all datasources, try to extract the latest tx
Set<String> noTxPresent = new HashSet<String>();
for ( XaDataSource ds : targetDb.getXaDataSourceManager().getAllRegisteredDataSources() )
{
long lastTx = ds.getLastCommittedTxId();
try
{
// This fails if the tx is not present with NSLVE
ds.getMasterForCommittedTx( lastTx );
}
catch ( NoSuchLogVersionException e )
{
// Note the name of the datasource
noTxPresent.add( ds.getName() );
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
}
if ( !noTxPresent.isEmpty() )
{
/*
* Create a fake slave context, asking for the transactions that
* span the next-to-last up to the latest for each datasource
*/
BackupClient recoveryClient = new BackupClient(
sourceHostNameOrIp, sourcePort, targetDb.getMessageLog(), targetDb.getStoreId() );
recoveryClient.start();
Response<Void> recoveryResponse = null;
Map<String, Long> recoveryDiff = new HashMap<String, Long>();
for ( String ds : noTxPresent )
{
recoveryDiff.put( ds, -1L );
}
RequestContext recoveryCtx = addDiffToSlaveContext(
slaveContextOf( targetDb ), recoveryDiff );
try
{
recoveryResponse = recoveryClient.incrementalBackup( recoveryCtx );
// Ok, the response is here, apply it.
TransactionStream txs = recoveryResponse.transactions();
ByteBuffer scratch = ByteBuffer.allocate( 64 );
while ( txs.hasNext() )
{
/*
* For each tx stream in the response, create the latest archived
* logical log file and write out in there the transaction.
*
*/
Triplet<String, Long, TxExtractor> tx = txs.next();
scratch.clear();
XaDataSource ds = targetDb.getXaDataSourceManager().getXaDataSource(
tx.first() );
long logVersion = ds.getCurrentLogVersion() - 1;
FileChannel newLog = new RandomAccessFile(
ds.getFileName( logVersion ),
"rw" ).getChannel();
newLog.truncate( 0 );
LogIoUtils.writeLogHeader( scratch, logVersion, -1 );
// scratch buffer is flipped by writeLogHeader
newLog.write( scratch );
ReadableByteChannel received = tx.third().extract();
scratch.flip();
while ( received.read( scratch ) > 0 )
{
scratch.flip();
newLog.write( scratch );
scratch.flip();
}
newLog.force( false );
newLog.close();
received.close();
}
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
finally
{
try
{
recoveryClient.stop();
}
catch ( Throwable throwable )
{
throw new RuntimeException( throwable );
}
if ( recoveryResponse != null )
{
recoveryResponse.close();
}
targetDb.shutdown();
}
}
}
finally
{
targetDb.shutdown();
}
bumpLogFile( targetDirectory, timestamp );
if ( checkConsistency )
{
StringLogger logger = StringLogger.SYSTEM;
try
{
new ConsistencyCheckService().runFullConsistencyCheck(
targetDirectory,
tuningConfiguration,
ProgressMonitorFactory.textual( System.err ),
logger );
}
catch ( ConsistencyCheckIncompleteException e )
{
e.printStackTrace( System.err );
}
finally
{
logger.flush();
}
}
}
finally
{
try
{
client.stop();
}
catch ( Throwable throwable )
{
throw new RuntimeException( throwable );
}
}
return new BackupOutcome( lastCommittedTxs );
}
BackupOutcome doIncrementalBackup( String sourceHostNameOrIp, int sourcePort, String targetDirectory,
boolean verification )
{
if ( !directoryContainsDb( targetDirectory ) )
{
throw new RuntimeException( targetDirectory + " doesn't contain a database" );
}
// In case someone deleted the logical log from a full backup
ConfigParam keepLogs = new ConfigParam()
{
@Override
public void configure( Map<String, String> config )
{
config.put( GraphDatabaseSettings.keep_logical_logs.name(), GraphDatabaseSetting.TRUE );
}
};
GraphDatabaseAPI targetDb = startTemporaryDb( targetDirectory,
VerificationLevel.valueOf( verification ), keepLogs );
long backupStartTime = System.currentTimeMillis();
BackupOutcome outcome = null;
try
{
outcome = doIncrementalBackup( sourceHostNameOrIp, sourcePort, targetDb );
}
finally
{
targetDb.shutdown();
}
bumpLogFile( targetDirectory, backupStartTime );
return outcome;
}
BackupOutcome doIncrementalBackup( String sourceHostNameOrIp, int sourcePort, GraphDatabaseAPI targetDb )
{
return incrementalWithContext( sourceHostNameOrIp, sourcePort, targetDb, slaveContextOf( targetDb ) );
}
private RequestContext slaveContextOf( GraphDatabaseAPI graphDb )
{
XaDataSourceManager dsManager = graphDb.getXaDataSourceManager();
List<Tx> txs = new ArrayList<Tx>();
for ( XaDataSource ds : dsManager.getAllRegisteredDataSources() )
{
txs.add( RequestContext.lastAppliedTx( ds.getName(), ds.getLastCommittedTxId() ) );
}
return RequestContext.anonymous( txs.toArray( new Tx[0] ) );
}
private StoreWriter decorateWithProgressIndicator( final StoreWriter actual )
{
return new StoreWriter()
{
private final ProgressIndicator progress = new ProgressIndicator.UnknownEndProgress( 1, "Files copied" );
private int totalFiles;
@Override
public void write( String path, ReadableByteChannel data, ByteBuffer temporaryBuffer,
boolean hasData ) throws IOException
{
actual.write( path, data, temporaryBuffer, hasData );
progress.update( true, 1 );
totalFiles++;
}
@Override
public void done()
{
actual.done();
progress.done( totalFiles );
}
};
}
static boolean directoryContainsDb( String targetDirectory )
{
return new File( targetDirectory, NeoStore.DEFAULT_NAME ).exists();
}
static EmbeddedGraphDatabase startTemporaryDb( String targetDirectory, ConfigParam... params )
{
if ( params != null && params.length > 0 )
{
Map<String, String> config = new HashMap<String, String>();
for ( ConfigParam param : params )
{
if ( param != null )
{
param.configure( config );
}
}
return new EmbeddedGraphDatabase( targetDirectory, config );
}
else
{
return new EmbeddedGraphDatabase( targetDirectory );
}
}
private RequestContext addDiffToSlaveContext( RequestContext original,
Map<String, Long> diffPerDataSource )
{
Tx[] oldTxs = original.lastAppliedTransactions();
Tx[] newTxs = new Tx[oldTxs.length];
for ( int i = 0; i < oldTxs.length; i++ )
{
Tx oldTx = oldTxs[i];
String dsName = oldTx.getDataSourceName();
long originalTxId = oldTx.getTxId();
Long diff = diffPerDataSource.get( dsName );
if ( diff == null )
{
diff = Long.valueOf( 0L );
}
long newTxId = originalTxId + diff;
newTxs[i] = RequestContext.lastAppliedTx( dsName, newTxId );
}
return RequestContext.anonymous( newTxs );
}
/**
* Performs an incremental backup based off the given context. This means
* receiving and applying selectively (i.e. irrespective of the actual state
* of the target db) a set of transactions starting at the desired txId and
* spanning up to the latest of the master, for every data source
* registered.
*
* @param sourceHostNameOrIp
* @param sourcePort
* @param targetDb The database that contains a previous full copy
* @param context The context, i.e. a mapping of data source name to txid
* which will be the first in the returned stream
* @return A backup context, ready to perform
*/
private BackupOutcome incrementalWithContext( String sourceHostNameOrIp, int sourcePort, GraphDatabaseAPI targetDb,
RequestContext context )
{
BackupClient client = new BackupClient( sourceHostNameOrIp, sourcePort, targetDb.getMessageLog(),
targetDb.getStoreId() );
client.start();
Map<String, Long> lastCommittedTxs;
try
{
lastCommittedTxs = unpackResponse( client.incrementalBackup( context ),
targetDb.getDependencyResolver().resolveDependency( XaDataSourceManager.class ),
new ProgressTxHandler() );
trimLogicalLogCount( targetDb );
}
finally
{
try
{
client.stop();
}
catch ( Throwable throwable )
{
throw new RuntimeException( throwable );
}
}
return new BackupOutcome( lastCommittedTxs );
}
private void trimLogicalLogCount( GraphDatabaseAPI targetDb )
{
for ( XaDataSource ds : targetDb.getXaDataSourceManager().getAllRegisteredDataSources() )
{
try
{
ds.rotateLogicalLog();
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
long currentVersion = ds.getCurrentLogVersion() - 1;
// TODO
/*
* Checking the file size to determine if transactions exist in
* a log feels hack-ish. Maybe fix this to read the header
* and check latest txid?
*/
while ( ds.getLogicalLogLength( currentVersion ) <= 16
&& currentVersion > 0 )
{
currentVersion--;
}
/*
* Ok, we skipped all logs that have no transactions in them. Current is the
* one with the tx in it. Skip it.
*/
currentVersion--;
/*
* Now delete the rest.
*/
while ( ds.getLogicalLogLength( currentVersion ) > 0 )
{
ds.deleteLogicalLog( currentVersion );
currentVersion--;
}
}
}
private Map<String, Long> unpackResponse( Response<Void> response, XaDataSourceManager xaDsm, TxHandler txHandler )
{
try
{
ServerUtil.applyReceivedTransactions( response, xaDsm, txHandler );
return extractLastCommittedTxs( xaDsm );
}
catch ( IOException e )
{
throw new RuntimeException( "Unable to apply received transactions", e );
}
}
private Map<String, Long> extractLastCommittedTxs( XaDataSourceManager xaDsm )
{
TreeMap<String, Long> lastCommittedTxs = new TreeMap<String, Long>();
for ( XaDataSource ds : xaDsm.getAllRegisteredDataSources() )
{
lastCommittedTxs.put( ds.getName(), ds.getLastCommittedTxId() );
}
return lastCommittedTxs;
}
private static boolean bumpLogFile( String targetDirectory, long toTimestamp )
{
File dbDirectory = new File( targetDirectory );
File[] candidates = dbDirectory.listFiles( new FilenameFilter()
{
@Override
public boolean accept( File dir, String name )
{
/*
* Contains ensures that previously timestamped files are
* picked up as well
*/
return name.equals( StringLogger.DEFAULT_NAME );
}
} );
File previous = null;
if ( candidates.length != 1 )
{
return false;
}
// candidates has a unique member, the right one
else
{
previous = candidates[0];
}
// Build to, from existing parent + new filename
File to = new File( previous.getParentFile(), StringLogger.DEFAULT_NAME
+ "." + toTimestamp );
return previous.renameTo( to );
}
private static class ProgressTxHandler implements TxHandler
{
private final ProgressIndicator progress = new ProgressIndicator.UnknownEndProgress( 1000,
"Transactions applied" );
private long count;
@Override
public void accept( Triplet<String, Long, TxExtractor> tx, XaDataSource dataSource )
{
progress.update( true, 1 );
count++;
}
@Override
public void done()
{
progress.done( count );
}
}
}