blob: cd67da9a1e9991d5c76f0e908a7b5e93bb108393 [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.qpid.server.store.berkeleydb;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.util.DbBackup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.qpid.server.store.StoreException;
import org.apache.qpid.server.util.CommandLineParser;
import org.apache.qpid.server.util.FileUtils;
/**
* BDBBackup is a utility for taking hot backups of the current state of a BDB transaction log database.
* <p>
* This utility makes the following assumptions/performs the following actions:
* <p>
* <ul> <li>The from and to directory locations will already exist. This scripts does not create them. <li>If this
* script fails to complete in one minute it will terminate. <li>This script always exits with code 1 on error, code 0
* on success (standard unix convention). <li>This script will log out at info level, when it starts and ends and a list
* of all files backed up. <li>This script logs all errors at error level. <li>This script does not perform regular
* backups, wrap its calling script in a cron job or similar to do this. </ul>
* <p>
* This utility is build around the BDB provided backup helper utility class, DbBackup. This utility class provides
* an ability to force BDB to stop writing to the current log file set, whilst the backup is taken, to ensure that a
* consistent snapshot is acquired. Preventing BDB from writing to the current log file set, does not stop BDB from
* continuing to run concurrently while the backup is running, it simply moves onto a new set of log files; this
* provides a 'hot' backup facility.
* <p>
* DbBackup can also help with incremental backups, by providing the number of the last log file backed up.
* Subsequent backups can be taken, from later log files only. In a messaging application, messages are not expected to
* be long-lived in most cases, so the log files will usually have been completely turned over between backups. This
* utility does not support incremental backups for this reason.
* <p>
* If the database is locked by BDB, as is required when using transactions, and therefore will always be the case
* in Qpid, this utility cannot make use of the DbBackup utility in a seperate process. DbBackup, needs to ensure that
* the BDB envinronment used to take the backup has exclusive write access to the log files. This utility can take a
* backup as a standalone utility against log files, when a broker is not running, using the {@link #takeBackup(String,
*String,com.sleepycat.je.Environment)} method.
* <p>
* A separate backup machanism is provided by the {@link #takeBackupNoLock(String,String)} method which can take a
* hot backup against a running broker. This works by finding out the set of files to copy, and then opening them all to
* read, and repeating this process until a consistent set of open files is obtained. This is done to avoid the
* situation where the BDB cleanup thread deletes a file, between the directory listing and opening of the file to copy.
* All consistently opened files are copied. This is the default mechanism the the {@link #main} method of this utility
* uses.
*/
public class BDBBackup
{
/** Used for debugging. */
private static final Logger log = LoggerFactory.getLogger(BDBBackup.class);
/** Used for communicating with the user. */
private static final Logger console = LoggerFactory.getLogger("Console");
/** Defines the suffix used to identify BDB log files. */
private static final String LOG_FILE_SUFFIX = ".jdb";
/** Defines the command line format for this utility. */
public static final String[][] COMMAND_LINE_SPEC =
new String[][]
{
{ "fromdir", "The path to the directory to back the bdb log file from.", "dir", "true" },
{ "todir", "The path to the directory to save the backed up bdb log files to.", "dir", "true" }
};
/** Defines the timeout to terminate the backup operation on if it fails to complete. One minute. */
public static final long TIMEOUT = 60000;
/**
* Runs a backup of the BDB log files in a specified directory, copying the backed up files to another specified
* directory.
* <p>
* The following arguments must be specified:
* <table>
* <caption>Command Line</caption> <tr><th> Option <th> Comment <tr><td> -fromdir <td> The path to the
* directory to back the bdb log file from. <tr><td> -todir <td> The path to the directory to save the backed up
* bdb log files to. </table>
*
* @param args The command line arguments.
*/
public static void main(String[] args)
{
// Process the command line using standard handling (errors and usage followed by System.exit when it is wrong).
Properties options =
CommandLineParser.processCommandLine(args, new CommandLineParser(COMMAND_LINE_SPEC), System.getProperties());
// Extract the from and to directory locations and perform a backup between them.
try
{
String fromDir = options.getProperty("fromdir");
String toDir = options.getProperty("todir");
log.info("BDBBackup Utility: Starting Hot Backup.");
BDBBackup bdbBackup = new BDBBackup();
String[] backedUpFiles = bdbBackup.takeBackupNoLock(fromDir, toDir);
if (log.isInfoEnabled())
{
log.info("BDBBackup Utility: Hot Backup Completed.");
log.info(backedUpFiles.length + " file(s) backed-up:");
for(String backedUpFile : backedUpFiles)
{
log.info(backedUpFile);
}
}
}
catch (Exception e)
{
console.info("Backup script encountered an error and has failed: " + e.getMessage());
log.error("Backup script got exception: " + e.getMessage(), e);
System.exit(1);
}
}
/**
* Creates a backup of the BDB log files in the source directory, copying them to the destination directory.
*
* @param fromdir The source directory path.
* @param todir The destination directory path.
* @param environment An open BDB environment to perform the back up.
*
* @throws DatabaseException Any underlying execeptions from BDB are allowed to fall through.
*/
public void takeBackup(String fromdir, String todir, Environment environment) throws DatabaseException
{
DbBackup backupHelper = null;
try
{
backupHelper = new DbBackup(environment);
// Prevent BDB from writing to its log files while the backup it taken.
backupHelper.startBackup();
// Back up the BDB log files to the destination directory.
String[] filesForBackup = backupHelper.getLogFilesInBackupSet();
for (int i = 0; i < filesForBackup.length; i++)
{
File sourceFile = new File(fromdir + File.separator + filesForBackup[i]);
File destFile = new File(todir + File.separator + filesForBackup[i]);
FileUtils.copy(sourceFile, destFile);
}
}
finally
{
// Remember to exit backup mode, or all log files won't be cleaned and disk usage will bloat.
if (backupHelper != null)
{
backupHelper.endBackup();
}
}
}
/**
* Takes a hot backup when another process has locked the BDB database.
*
* @param fromdir The source directory path.
* @param todir The destination directory path.
*
* @return A list of all of the names of the files succesfully backed up.
*/
public String[] takeBackupNoLock(String fromdir, String todir)
{
if (log.isDebugEnabled())
{
log.debug("public void takeBackupNoLock(String fromdir = " + fromdir + ", String todir = " + todir
+ "): called");
}
File fromDirFile = new File(fromdir);
if (!fromDirFile.isDirectory())
{
throw new IllegalArgumentException("The specified fromdir(" + fromdir
+ ") must be the directory containing your bdbstore.");
}
File toDirFile = new File(todir);
if (!toDirFile.exists())
{
// Create directory if it doesn't exist
toDirFile.mkdirs();
if (log.isDebugEnabled())
{
log.debug("Created backup directory:" + toDirFile);
}
}
if (!toDirFile.isDirectory())
{
throw new IllegalArgumentException("The specified todir(" + todir + ") must be a directory.");
}
// Repeat until manage to open consistent set of files for reading.
boolean consistentSet = false;
FileInputStream[] fileInputStreams = new FileInputStream[0];
File[] fileSet = new File[0];
long start = System.currentTimeMillis();
while (!consistentSet)
{
// List all .jdb files in the directory.
fileSet = fromDirFile.listFiles(new FilenameFilter()
{
@Override
public boolean accept(File dir, String name)
{
return name.endsWith(LOG_FILE_SUFFIX);
}
});
if (fileSet == null || fileSet.length == 0)
{
throw new StoreException("There are no BDB log files to backup in the '" + fromdir + "' directory.");
}
// The files must be copied in alphabetical order (numerical in effect)
Arrays.sort(fileSet);
// Open them all for reading.
fileInputStreams = new FileInputStream[fileSet.length];
for (int i = 0; i < fileSet.length; i++)
{
try
{
fileInputStreams[i] = new FileInputStream(fileSet[i]);
}
catch (FileNotFoundException e)
{
// Close any files opened for reading so far.
for (int j = 0; j < i; j++)
{
if (fileInputStreams[j] != null)
{
try
{
fileInputStreams[j].close();
}
catch (IOException ioEx)
{
// Rethrow this as a runtime exception, as something strange has happened.
throw new StoreException(ioEx);
}
}
}
// Could not open a consistent file set so try again.
break;
}
// A consistent set has been opened if all files were successfully opened for reading.
if (i == (fileSet.length - 1))
{
consistentSet = true;
}
}
// Check that the script has not timed out, and raise an error if it has.
long now = System.currentTimeMillis();
if ((now - start) > TIMEOUT)
{
throw new StoreException("Hot backup script failed to complete in " + (TIMEOUT / 1000) + " seconds.");
}
}
// Copy the consistent set of open files.
List<String> backedUpFileNames = new LinkedList<String>();
for (int j = 0; j < fileSet.length; j++)
{
File destFile = new File(todir + File.separator + fileSet[j].getName());
try
{
Files.copy(fileInputStreams[j], destFile.toPath());
}
catch (IOException ioe)
{
throw new StoreException(ioe.getMessage() + " fromDir:" + fromdir + " toDir:" + toDirFile, ioe);
}
backedUpFileNames.add(destFile.getName());
// Close all of the files.
try
{
fileInputStreams[j].close();
}
catch (IOException e)
{
// Rethrow this as a runtime exception, as something strange has happened.
throw new StoreException(e);
}
}
return backedUpFileNames.toArray(new String[backedUpFileNames.size()]);
}
/*
* Creates an environment for the bdb log files in the specified directory. This envrinonment can only be used
* to backup these files, if they are not locked by another database instance.
*
* @param fromdir The path to the directory to create the environment for.
*
* @throws DatabaseException Any underlying exceptions from BDB are allowed to fall through.
*/
private Environment createSourceDirEnvironment(String fromdir) throws DatabaseException
{
// Initialize the BDB backup utility on the source directory.
return new Environment(new File(fromdir), new EnvironmentConfig());
}
}