package voldemort.store.bdb; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.PrintStream; import java.nio.channels.FileChannel; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import voldemort.VoldemortException; import voldemort.server.protocol.admin.AsyncOperationStatus; import com.sleepycat.je.Environment; import com.sleepycat.je.util.DbBackup; import com.sleepycat.je.util.LogVerificationInputStream; /** * Handles native backups for the BDB storage engine */ public class BdbNativeBackup { private static final String BDB_EXT = ".jdb"; private static final int LOGVERIFY_BUFSIZE = 1024; private final Environment env; private final File databaseDir; private final boolean verifyFiles; private final boolean isIncremental; private DbBackup backupHelper; public BdbNativeBackup(Environment env, boolean verifyFiles, boolean isIncremental) { this.env = env; this.verifyFiles = verifyFiles; this.isIncremental = isIncremental; this.databaseDir = env.getHome(); } public void performBackup(File backupDir, AsyncOperationStatus status) { // Find the file number of the last file in the previous backup // persistently, by either checking the backup archive, or saving // state in a persistent file. Long lastFileInPrevBackup = determineLastFile(backupDir, status); try { if(lastFileInPrevBackup == null) { backupHelper = new DbBackup(env); } else { backupHelper = new DbBackup(env, lastFileInPrevBackup); } // Start backup, find out what needs to be copied. System.out.println("Native backup started at " + new Date().toString()); backupHelper.startBackup(); try { String[] filesForBackup = backupHelper.getLogFilesInBackupSet(); // Copy the files to archival storage. backupFiles(filesForBackup, backupDir, status); } finally { // Remember to exit backup mode, or all log files won't be // cleaned and disk usage will bloat. backupHelper.endBackup(); System.out.println("Native backup completed at " + new Date().toString()); } } catch(Exception e) { throw new VoldemortException("Error performing native backup", e); } } private Long determineLastFile(File backupDir, final AsyncOperationStatus status) { status.setStatus("Determining the last backed up file..."); System.out.println("Backup directory is \'" + backupDir.getPath() + "\'."); File[] backupFiles = backupDir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { if(!name.endsWith(BDB_EXT)) { return false; } String part = name.substring(0, name.length() - BDB_EXT.length()); try { Long.parseLong(part, 16); } catch(NumberFormatException nfe) { status.setStatus("Warning: " + BDB_EXT + " file whose name is not a number, ignoring: " + name); return false; } return true; } }); if(backupFiles == null) { throw new VoldemortException("Failed to read backup directory. Please check" + "if the directory exists, or permission is granted."); } if(backupFiles.length == 0) { status.setStatus("No backup files found, assuming a full backup is required."); return null; } long largest = Long.MIN_VALUE; for(File file: backupFiles) { long value = fileNameToNumber(file.getName()); if(value > largest) { largest = value; } } status.setStatus("Last backed up file was " + largest); return largest; } private void backupFiles(String[] filesForBackup, File backupDir, AsyncOperationStatus status) { // Determine size of backup long size = 0; for(String name: filesForBackup) { size += new File(databaseDir, name).length(); } status.setStatus(String.format("Backing up %d files with a total of %.1fMB", filesForBackup.length, mb(size))); // Ensure files are sorted in order, so that if we fail part way // through, we don't lose stuff Arrays.sort(filesForBackup, new Comparator<String>() { public int compare(String o1, String o2) { long result = fileNameToNumber(o1) - fileNameToNumber(o2); if(result < 0) { return -1; } else if(result > 0) { return 1; } return 0; } }); long total = 0; for(String name: filesForBackup) { File source = new File(databaseDir, name); File dest = new File(backupDir, name); status.setStatus(String.format("% 3d%% Copying %s", total * 100 / size, name)); try { if(verifyFiles) { verifiedCopyFile(source, dest); } else { copyFile(source, dest); } } catch(IOException e) { // If the destination file exists, delete it if(dest.exists()) { dest.delete(); } throw new VoldemortException("Error occurred while copying " + name + ". Deleting to ensure we don't have a corrupt backup.", e); } total += source.length(); } if(isIncremental) { try { recordBackupSet(backupDir); } catch(IOException e) { throw new VoldemortException("Error attempting to write backup records for ", e); } } else { cleanStaleFiles(backupDir, status); } } /** * Records the list of backedup files into a text file * * @param filesInEnv * @param backupDir */ private void recordBackupSet(File backupDir) throws IOException { String[] filesInEnv = env.getHome().list(); SimpleDateFormat format = new SimpleDateFormat("yyyy_MM_dd_kk_mm_ss"); String recordFileName = "backupset-" + format.format(new Date()); File recordFile = new File(backupDir, recordFileName); if(recordFile.exists()) { recordFile.renameTo(new File(backupDir, recordFileName + ".old")); } PrintStream backupRecord = new PrintStream(new FileOutputStream(recordFile)); backupRecord.println("Lastfile:" + Long.toHexString(backupHelper.getLastFileInBackupSet())); if(filesInEnv != null) { for(String file: filesInEnv) { if(file.endsWith(BDB_EXT)) backupRecord.println(file); } } backupRecord.close(); } /** * For recovery from the latest consistent snapshot, we should clean up the * old files from the previous backup set, else we will fill the disk with * useless log files * * @param backupDir */ private void cleanStaleFiles(File backupDir, AsyncOperationStatus status) { String[] filesInEnv = env.getHome().list(); String[] filesInBackupDir = backupDir.list(); if(filesInEnv != null && filesInBackupDir != null) { HashSet<String> envFileSet = new HashSet<String>(); for(String file: filesInEnv) envFileSet.add(file); // delete all files in backup which are currently not in environment for(String file: filesInBackupDir) { if(file.endsWith(BDB_EXT) && !envFileSet.contains(file)) { status.setStatus("Deleting stale jdb file :" + file); File staleJdbFile = new File(backupDir, file); staleJdbFile.delete(); } } } } /** * File copy using fast NIO zero copy method * * @param sourceFile * @param destFile * @throws IOException */ private void copyFile(File sourceFile, File destFile) throws IOException { if(!destFile.exists()) { destFile.createNewFile(); } FileChannel source = null; FileChannel destination = null; try { source = new FileInputStream(sourceFile).getChannel(); destination = new FileOutputStream(destFile).getChannel(); destination.transferFrom(source, 0, source.size()); } finally { if(source != null) { source.close(); } if(destination != null) { destination.close(); } } } /** * Copies the jdb log files, with additional verification of the checksums. * * @param sourceFile * @param destFile * @throws IOException */ private void verifiedCopyFile(File sourceFile, File destFile) throws IOException { if(!destFile.exists()) { destFile.createNewFile(); } FileInputStream source = null; FileOutputStream destination = null; LogVerificationInputStream verifyStream = null; try { source = new FileInputStream(sourceFile); destination = new FileOutputStream(destFile); verifyStream = new LogVerificationInputStream(env, source, sourceFile.getName()); final byte[] buf = new byte[LOGVERIFY_BUFSIZE]; while(true) { final int len = verifyStream.read(buf); if(len < 0) { break; } destination.write(buf, 0, len); } } finally { if(verifyStream != null) { verifyStream.close(); } if(destination != null) { destination.close(); } } } private static long fileNameToNumber(String name) { String part = name.substring(0, name.length() - BDB_EXT.length()); return Long.parseLong(part, 16); } private static double mb(long value) { return ((double) value) / 1048576; } }