Java tutorial
/** * 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.hadoop.hbase.backup.impl; import java.io.IOException; 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 org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.backup.BackupAdmin; import org.apache.hadoop.hbase.backup.BackupClientFactory; import org.apache.hadoop.hbase.backup.BackupInfo; import org.apache.hadoop.hbase.backup.BackupInfo.BackupState; import org.apache.hadoop.hbase.backup.BackupMergeJob; import org.apache.hadoop.hbase.backup.BackupRequest; import org.apache.hadoop.hbase.backup.BackupRestoreConstants; import org.apache.hadoop.hbase.backup.BackupRestoreFactory; import org.apache.hadoop.hbase.backup.BackupType; import org.apache.hadoop.hbase.backup.HBackupFileSystem; import org.apache.hadoop.hbase.backup.RestoreRequest; import org.apache.hadoop.hbase.backup.util.BackupSet; import org.apache.hadoop.hbase.backup.util.BackupUtils; import org.apache.hadoop.hbase.client.Admin; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hbase.thirdparty.com.google.common.collect.Lists; @InterfaceAudience.Private public class BackupAdminImpl implements BackupAdmin { public final static String CHECK_OK = "Checking backup images: OK"; public final static String CHECK_FAILED = "Checking backup images: Failed. Some dependencies are missing for restore"; private static final Logger LOG = LoggerFactory.getLogger(BackupAdminImpl.class); private final Connection conn; public BackupAdminImpl(Connection conn) { this.conn = conn; } @Override public void close() { } @Override public BackupInfo getBackupInfo(String backupId) throws IOException { BackupInfo backupInfo; try (final BackupSystemTable table = new BackupSystemTable(conn)) { if (backupId == null) { ArrayList<BackupInfo> recentSessions = table.getBackupInfos(BackupState.RUNNING); if (recentSessions.isEmpty()) { LOG.warn("No ongoing sessions found."); return null; } // else show status for ongoing session // must be one maximum return recentSessions.get(0); } else { backupInfo = table.readBackupInfo(backupId); return backupInfo; } } } @Override public int deleteBackups(String[] backupIds) throws IOException { int totalDeleted = 0; Map<String, HashSet<TableName>> allTablesMap = new HashMap<>(); boolean deleteSessionStarted; boolean snapshotDone; try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) { // Step 1: Make sure there is no active session // is running by using startBackupSession API // If there is an active session in progress, exception will be thrown try { sysTable.startBackupExclusiveOperation(); deleteSessionStarted = true; } catch (IOException e) { LOG.warn("You can not run delete command while active backup session is in progress. \n" + "If there is no active backup session running, run backup repair utility to " + "restore \nbackup system integrity."); return -1; } // Step 2: Make sure there is no failed session List<BackupInfo> list = sysTable.getBackupInfos(BackupState.RUNNING); if (list.size() != 0) { // ailed sessions found LOG.warn("Failed backup session found. Run backup repair tool first."); return -1; } // Step 3: Record delete session sysTable.startDeleteOperation(backupIds); // Step 4: Snapshot backup system table if (!BackupSystemTable.snapshotExists(conn)) { BackupSystemTable.snapshot(conn); } else { LOG.warn("Backup system table snapshot exists"); } snapshotDone = true; try { for (int i = 0; i < backupIds.length; i++) { BackupInfo info = sysTable.readBackupInfo(backupIds[i]); if (info != null) { String rootDir = info.getBackupRootDir(); HashSet<TableName> allTables = allTablesMap.get(rootDir); if (allTables == null) { allTables = new HashSet<>(); allTablesMap.put(rootDir, allTables); } allTables.addAll(info.getTableNames()); totalDeleted += deleteBackup(backupIds[i], sysTable); } } finalizeDelete(allTablesMap, sysTable); // Finish sysTable.finishDeleteOperation(); // delete snapshot BackupSystemTable.deleteSnapshot(conn); } catch (IOException e) { // Fail delete operation // Step 1 if (snapshotDone) { if (BackupSystemTable.snapshotExists(conn)) { BackupSystemTable.restoreFromSnapshot(conn); // delete snapshot BackupSystemTable.deleteSnapshot(conn); // We still have record with unfinished delete operation LOG.error("Delete operation failed, please run backup repair utility to restore " + "backup system integrity", e); throw e; } else { LOG.warn("Delete operation succeeded, there were some errors: ", e); } } } finally { if (deleteSessionStarted) { sysTable.finishBackupExclusiveOperation(); } } } return totalDeleted; } /** * Updates incremental backup set for every backupRoot * @param tablesMap map [backupRoot: {@code Set<TableName>}] * @param table backup system table * @throws IOException if a table operation fails */ private void finalizeDelete(Map<String, HashSet<TableName>> tablesMap, BackupSystemTable table) throws IOException { for (String backupRoot : tablesMap.keySet()) { Set<TableName> incrTableSet = table.getIncrementalBackupTableSet(backupRoot); Map<TableName, ArrayList<BackupInfo>> tableMap = table.getBackupHistoryForTableSet(incrTableSet, backupRoot); for (Map.Entry<TableName, ArrayList<BackupInfo>> entry : tableMap.entrySet()) { if (entry.getValue() == null) { // No more backups for a table incrTableSet.remove(entry.getKey()); } } if (!incrTableSet.isEmpty()) { table.addIncrementalBackupTableSet(incrTableSet, backupRoot); } else { // empty table.deleteIncrementalBackupTableSet(backupRoot); } } } /** * Delete single backup and all related backups <br> * Algorithm:<br> * Backup type: FULL or INCREMENTAL <br> * Is this last backup session for table T: YES or NO <br> * For every table T from table list 'tables':<br> * if(FULL, YES) deletes only physical data (PD) <br> * if(FULL, NO), deletes PD, scans all newer backups and removes T from backupInfo,<br> * until we either reach the most recent backup for T in the system or FULL backup<br> * which includes T<br> * if(INCREMENTAL, YES) deletes only physical data (PD) if(INCREMENTAL, NO) deletes physical data * and for table T scans all backup images between last<br> * FULL backup, which is older than the backup being deleted and the next FULL backup (if exists) * <br> * or last one for a particular table T and removes T from list of backup tables. * @param backupId backup id * @param sysTable backup system table * @return total number of deleted backup images * @throws IOException if deleting the backup fails */ private int deleteBackup(String backupId, BackupSystemTable sysTable) throws IOException { BackupInfo backupInfo = sysTable.readBackupInfo(backupId); int totalDeleted = 0; if (backupInfo != null) { LOG.info("Deleting backup " + backupInfo.getBackupId() + " ..."); // Step 1: clean up data for backup session (idempotent) BackupUtils.cleanupBackupData(backupInfo, conn.getConfiguration()); // List of tables in this backup; List<TableName> tables = backupInfo.getTableNames(); long startTime = backupInfo.getStartTs(); for (TableName tn : tables) { boolean isLastBackupSession = isLastBackupSession(sysTable, tn, startTime); if (isLastBackupSession) { continue; } // else List<BackupInfo> affectedBackups = getAffectedBackupSessions(backupInfo, tn, sysTable); for (BackupInfo info : affectedBackups) { if (info.equals(backupInfo)) { continue; } removeTableFromBackupImage(info, tn, sysTable); } } Map<byte[], String> map = sysTable.readBulkLoadedFiles(backupId); FileSystem fs = FileSystem.get(conn.getConfiguration()); boolean success = true; int numDeleted = 0; for (String f : map.values()) { Path p = new Path(f); try { LOG.debug("Delete backup info " + p + " for " + backupInfo.getBackupId()); if (!fs.delete(p)) { if (fs.exists(p)) { LOG.warn(f + " was not deleted"); success = false; } } else { numDeleted++; } } catch (IOException ioe) { LOG.warn(f + " was not deleted", ioe); success = false; } } if (LOG.isDebugEnabled()) { LOG.debug(numDeleted + " bulk loaded files out of " + map.size() + " were deleted"); } if (success) { sysTable.deleteBulkLoadedRows(new ArrayList<>(map.keySet())); } sysTable.deleteBackupInfo(backupInfo.getBackupId()); LOG.info("Delete backup " + backupInfo.getBackupId() + " completed."); totalDeleted++; } else { LOG.warn("Delete backup failed: no information found for backupID=" + backupId); } return totalDeleted; } private void removeTableFromBackupImage(BackupInfo info, TableName tn, BackupSystemTable sysTable) throws IOException { List<TableName> tables = info.getTableNames(); LOG.debug("Remove " + tn + " from " + info.getBackupId() + " tables=" + info.getTableListAsString()); if (tables.contains(tn)) { tables.remove(tn); if (tables.isEmpty()) { LOG.debug("Delete backup info " + info.getBackupId()); sysTable.deleteBackupInfo(info.getBackupId()); // Idempotent operation BackupUtils.cleanupBackupData(info, conn.getConfiguration()); } else { info.setTables(tables); sysTable.updateBackupInfo(info); // Now, clean up directory for table (idempotent) cleanupBackupDir(info, tn, conn.getConfiguration()); } } } private List<BackupInfo> getAffectedBackupSessions(BackupInfo backupInfo, TableName tn, BackupSystemTable table) throws IOException { LOG.debug("GetAffectedBackupInfos for: " + backupInfo.getBackupId() + " table=" + tn); long ts = backupInfo.getStartTs(); List<BackupInfo> list = new ArrayList<>(); List<BackupInfo> history = table.getBackupHistory(backupInfo.getBackupRootDir()); // Scan from most recent to backupInfo // break when backupInfo reached for (BackupInfo info : history) { if (info.getStartTs() == ts) { break; } List<TableName> tables = info.getTableNames(); if (tables.contains(tn)) { BackupType bt = info.getType(); if (bt == BackupType.FULL) { // Clear list if we encounter FULL backup list.clear(); } else { LOG.debug("GetAffectedBackupInfos for: " + backupInfo.getBackupId() + " table=" + tn + " added " + info.getBackupId() + " tables=" + info.getTableListAsString()); list.add(info); } } } return list; } /** * Clean up the data at target directory * @throws IOException if cleaning up the backup directory fails */ private void cleanupBackupDir(BackupInfo backupInfo, TableName table, Configuration conf) throws IOException { try { // clean up the data at target directory String targetDir = backupInfo.getBackupRootDir(); if (targetDir == null) { LOG.warn("No target directory specified for " + backupInfo.getBackupId()); return; } FileSystem outputFs = FileSystem.get(new Path(backupInfo.getBackupRootDir()).toUri(), conf); Path targetDirPath = new Path( BackupUtils.getTableBackupDir(backupInfo.getBackupRootDir(), backupInfo.getBackupId(), table)); if (outputFs.delete(targetDirPath, true)) { LOG.info("Cleaning up backup data at " + targetDirPath.toString() + " done."); } else { LOG.info("No data has been found in " + targetDirPath.toString() + "."); } } catch (IOException e1) { LOG.error("Cleaning up backup data of " + backupInfo.getBackupId() + " for table " + table + "at " + backupInfo.getBackupRootDir() + " failed due to " + e1.getMessage() + "."); throw e1; } } private boolean isLastBackupSession(BackupSystemTable table, TableName tn, long startTime) throws IOException { List<BackupInfo> history = table.getBackupHistory(); for (BackupInfo info : history) { List<TableName> tables = info.getTableNames(); if (!tables.contains(tn)) { continue; } return info.getStartTs() <= startTime; } return false; } @Override public List<BackupInfo> getHistory(int n) throws IOException { try (final BackupSystemTable table = new BackupSystemTable(conn)) { List<BackupInfo> history = table.getBackupHistory(); if (history.size() <= n) { return history; } List<BackupInfo> list = new ArrayList<>(); for (int i = 0; i < n; i++) { list.add(history.get(i)); } return list; } } @Override public List<BackupInfo> getHistory(int n, BackupInfo.Filter... filters) throws IOException { if (filters.length == 0) { return getHistory(n); } try (final BackupSystemTable table = new BackupSystemTable(conn)) { List<BackupInfo> history = table.getBackupHistory(); List<BackupInfo> result = new ArrayList<>(); for (BackupInfo bi : history) { if (result.size() == n) { break; } boolean passed = true; for (int i = 0; i < filters.length; i++) { if (!filters[i].apply(bi)) { passed = false; break; } } if (passed) { result.add(bi); } } return result; } } @Override public List<BackupSet> listBackupSets() throws IOException { try (final BackupSystemTable table = new BackupSystemTable(conn)) { List<String> list = table.listBackupSets(); List<BackupSet> bslist = new ArrayList<>(); for (String s : list) { List<TableName> tables = table.describeBackupSet(s); if (tables != null) { bslist.add(new BackupSet(s, tables)); } } return bslist; } } @Override public BackupSet getBackupSet(String name) throws IOException { try (final BackupSystemTable table = new BackupSystemTable(conn)) { List<TableName> list = table.describeBackupSet(name); if (list == null) { return null; } return new BackupSet(name, list); } } @Override public boolean deleteBackupSet(String name) throws IOException { try (final BackupSystemTable table = new BackupSystemTable(conn)) { if (table.describeBackupSet(name) == null) { return false; } table.deleteBackupSet(name); return true; } } @Override public void addToBackupSet(String name, TableName[] tables) throws IOException { String[] tableNames = new String[tables.length]; try (final BackupSystemTable table = new BackupSystemTable(conn); final Admin admin = conn.getAdmin()) { for (int i = 0; i < tables.length; i++) { tableNames[i] = tables[i].getNameAsString(); if (!admin.tableExists(TableName.valueOf(tableNames[i]))) { throw new IOException("Cannot add " + tableNames[i] + " because it doesn't exist"); } } table.addToBackupSet(name, tableNames); LOG.info("Added tables [" + StringUtils.join(tableNames, " ") + "] to '" + name + "' backup set"); } } @Override public void removeFromBackupSet(String name, TableName[] tables) throws IOException { LOG.info("Removing tables [" + StringUtils.join(tables, " ") + "] from '" + name + "'"); try (final BackupSystemTable table = new BackupSystemTable(conn)) { table.removeFromBackupSet(name, toStringArray(tables)); LOG.info("Removing tables [" + StringUtils.join(tables, " ") + "] from '" + name + "' completed."); } } private String[] toStringArray(TableName[] list) { String[] arr = new String[list.length]; for (int i = 0; i < list.length; i++) { arr[i] = list[i].toString(); } return arr; } @Override public void restore(RestoreRequest request) throws IOException { if (request.isCheck()) { HashMap<TableName, BackupManifest> backupManifestMap = new HashMap<>(); // check and load backup image manifest for the tables Path rootPath = new Path(request.getBackupRootDir()); String backupId = request.getBackupId(); TableName[] sTableArray = request.getFromTables(); HBackupFileSystem.checkImageManifestExist(backupManifestMap, sTableArray, conn.getConfiguration(), rootPath, backupId); // Check and validate the backup image and its dependencies if (BackupUtils.validate(backupManifestMap, conn.getConfiguration())) { LOG.info(CHECK_OK); } else { LOG.error(CHECK_FAILED); } return; } // Execute restore request new RestoreTablesClient(conn, request).execute(); } @Override public String backupTables(BackupRequest request) throws IOException { BackupType type = request.getBackupType(); String targetRootDir = request.getTargetRootDir(); List<TableName> tableList = request.getTableList(); String backupId = BackupRestoreConstants.BACKUPID_PREFIX + EnvironmentEdgeManager.currentTime(); if (type == BackupType.INCREMENTAL) { Set<TableName> incrTableSet; try (BackupSystemTable table = new BackupSystemTable(conn)) { incrTableSet = table.getIncrementalBackupTableSet(targetRootDir); } if (incrTableSet.isEmpty()) { String msg = "Incremental backup table set contains no tables. " + "You need to run full backup first " + (tableList != null ? "on " + StringUtils.join(tableList, ",") : ""); throw new IOException(msg); } if (tableList != null) { tableList.removeAll(incrTableSet); if (!tableList.isEmpty()) { String extraTables = StringUtils.join(tableList, ","); String msg = "Some tables (" + extraTables + ") haven't gone through full backup. " + "Perform full backup on " + extraTables + " first, " + "then retry the command"; throw new IOException(msg); } } tableList = Lists.newArrayList(incrTableSet); } if (tableList != null && !tableList.isEmpty()) { for (TableName table : tableList) { String targetTableBackupDir = HBackupFileSystem.getTableBackupDir(targetRootDir, backupId, table); Path targetTableBackupDirPath = new Path(targetTableBackupDir); FileSystem outputFs = FileSystem.get(targetTableBackupDirPath.toUri(), conn.getConfiguration()); if (outputFs.exists(targetTableBackupDirPath)) { throw new IOException("Target backup directory " + targetTableBackupDir + " exists already."); } outputFs.mkdirs(targetTableBackupDirPath); } ArrayList<TableName> nonExistingTableList = null; try (Admin admin = conn.getAdmin()) { for (TableName tableName : tableList) { if (!admin.tableExists(tableName)) { if (nonExistingTableList == null) { nonExistingTableList = new ArrayList<>(); } nonExistingTableList.add(tableName); } } } if (nonExistingTableList != null) { if (type == BackupType.INCREMENTAL) { // Update incremental backup set tableList = excludeNonExistingTables(tableList, nonExistingTableList); } else { // Throw exception only in full mode - we try to backup non-existing table throw new IOException("Non-existing tables found in the table list: " + nonExistingTableList); } } } // update table list BackupRequest.Builder builder = new BackupRequest.Builder(); request = builder.withBackupType(request.getBackupType()).withTableList(tableList) .withTargetRootDir(request.getTargetRootDir()).withBackupSetName(request.getBackupSetName()) .withTotalTasks(request.getTotalTasks()).withBandwidthPerTasks((int) request.getBandwidth()) .build(); TableBackupClient client; try { client = BackupClientFactory.create(conn, backupId, request); } catch (IOException e) { LOG.error("There is an active session already running"); throw e; } client.execute(); return backupId; } private List<TableName> excludeNonExistingTables(List<TableName> tableList, List<TableName> nonExistingTableList) { for (TableName table : nonExistingTableList) { tableList.remove(table); } return tableList; } @Override public void mergeBackups(String[] backupIds) throws IOException { try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) { checkIfValidForMerge(backupIds, sysTable); //TODO run job on remote cluster BackupMergeJob job = BackupRestoreFactory.getBackupMergeJob(conn.getConfiguration()); job.run(backupIds); } } /** * Verifies that backup images are valid for merge. * * <ul> * <li>All backups MUST be in the same destination * <li>No FULL backups are allowed - only INCREMENTAL * <li>All backups must be in COMPLETE state * <li>No holes in backup list are allowed * </ul> * <p> * @param backupIds list of backup ids * @param table backup system table * @throws IOException if the backup image is not valid for merge */ private void checkIfValidForMerge(String[] backupIds, BackupSystemTable table) throws IOException { String backupRoot = null; final Set<TableName> allTables = new HashSet<>(); final Set<String> allBackups = new HashSet<>(); long minTime = Long.MAX_VALUE, maxTime = Long.MIN_VALUE; for (String backupId : backupIds) { BackupInfo bInfo = table.readBackupInfo(backupId); if (bInfo == null) { String msg = "Backup session " + backupId + " not found"; throw new IOException(msg); } if (backupRoot == null) { backupRoot = bInfo.getBackupRootDir(); } else if (!bInfo.getBackupRootDir().equals(backupRoot)) { throw new IOException("Found different backup destinations in a list of a backup sessions " + "\n1. " + backupRoot + "\n" + "2. " + bInfo.getBackupRootDir()); } if (bInfo.getType() == BackupType.FULL) { throw new IOException("FULL backup image can not be merged for: \n" + bInfo); } if (bInfo.getState() != BackupState.COMPLETE) { throw new IOException("Backup image " + backupId + " can not be merged becuase of its state: " + bInfo.getState()); } allBackups.add(backupId); allTables.addAll(bInfo.getTableNames()); long time = bInfo.getStartTs(); if (time < minTime) { minTime = time; } if (time > maxTime) { maxTime = time; } } final long startRangeTime = minTime; final long endRangeTime = maxTime; final String backupDest = backupRoot; // Check we have no 'holes' in backup id list // Filter 1 : backupRoot // Filter 2 : time range filter // Filter 3 : table filter BackupInfo.Filter destinationFilter = info -> info.getBackupRootDir().equals(backupDest); BackupInfo.Filter timeRangeFilter = info -> { long time = info.getStartTs(); return time >= startRangeTime && time <= endRangeTime; }; BackupInfo.Filter tableFilter = info -> { List<TableName> tables = info.getTableNames(); return !Collections.disjoint(allTables, tables); }; BackupInfo.Filter typeFilter = info -> info.getType() == BackupType.INCREMENTAL; BackupInfo.Filter stateFilter = info -> info.getState() == BackupState.COMPLETE; List<BackupInfo> allInfos = table.getBackupHistory(-1, destinationFilter, timeRangeFilter, tableFilter, typeFilter, stateFilter); if (allInfos.size() != allBackups.size()) { // Yes we have at least one hole in backup image sequence List<String> missingIds = new ArrayList<>(); for (BackupInfo info : allInfos) { if (allBackups.contains(info.getBackupId())) { continue; } missingIds.add(info.getBackupId()); } String errMsg = "Sequence of backup ids has 'holes'. The following backup images must be added:" + org.apache.hadoop.util.StringUtils.join(",", missingIds); throw new IOException(errMsg); } } }