diff --git a/hbase-backup/pom.xml b/hbase-backup/pom.xml
new file mode 100644
index 000000000000..e12d699184f5
--- /dev/null
+++ b/hbase-backup/pom.xml
@@ -0,0 +1,307 @@
+
+
+
+ 4.0.0
+
+ org.apache.hbase
+ hbase-build-configuration
+ ${revision}
+ ../hbase-build-configuration
+
+ hbase-backup
+ Apache HBase - Backup
+ Backup for HBase
+
+
+
+ org.apache.hbase
+ hbase-annotations
+ test-jar
+ test
+
+
+ org.apache.hbase
+ hbase-logging
+ test-jar
+ test
+
+
+ org.apache.hbase
+ hbase-client
+
+
+ org.apache.hbase
+ hbase-server
+
+
+ javax.xml.bind
+ jaxb-api
+
+
+ javax.ws.rs
+ jsr311-api
+
+
+
+
+ org.apache.hbase
+ hbase-server
+ test-jar
+ test
+
+
+ org.apache.hbase
+ hbase-mapreduce
+ test-jar
+ test
+
+
+ org.apache.hbase
+ hbase-mapreduce
+
+
+ org.apache.hbase
+ hbase-common
+
+
+ org.apache.hbase
+ hbase-protocol-shaded
+
+
+ org.apache.hbase
+ hbase-testing-util
+ test
+
+
+ com.google.guava
+ guava
+
+
+ javax.xml.bind
+ jaxb-api
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.apache.hbase.thirdparty
+ hbase-shaded-miscellaneous
+
+
+ org.apache.zookeeper
+ zookeeper
+
+
+ org.slf4j
+ jcl-over-slf4j
+ test
+
+
+ org.slf4j
+ jul-to-slf4j
+ test
+
+
+ org.apache.logging.log4j
+ log4j-api
+ test
+
+
+ org.apache.logging.log4j
+ log4j-core
+ test
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ test
+
+
+ org.apache.logging.log4j
+ log4j-1.2-api
+ test
+
+
+ junit
+ junit
+ test
+
+
+
+
+
+
+ maven-assembly-plugin
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ net.revelc.code
+ warbucks-maven-plugin
+
+
+
+
+
+
+ hadoop-2.0
+
+
+
+
+ !hadoop.profile
+
+
+
+
+ org.apache.hadoop
+ hadoop-distcp
+
+
+ org.apache.hadoop
+ hadoop-common
+
+
+ org.apache.hadoop
+ hadoop-mapreduce-client-core
+
+
+
+ io.netty
+ netty
+ ${netty.hadoop.version}
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ create-mrapp-generated-classpath
+
+ build-classpath
+
+ generate-test-resources
+
+
+ ${project.build.directory}/test-classes/mrapp-generated-classpath
+
+
+
+
+
+
+
+
+
+ hadoop-3.0
+
+
+ hadoop.profile
+ 3.0
+
+
+
+
+ org.apache.hadoop
+ hadoop-distcp
+
+
+ org.apache.hadoop
+ hadoop-common
+
+
+ org.apache.hadoop
+ hadoop-mapreduce-client-core
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ create-mrapp-generated-classpath
+
+ build-classpath
+
+ generate-test-resources
+
+
+ ${project.build.directory}/test-classes/mrapp-generated-classpath
+
+
+
+
+
+
+
+
+ eclipse-specific
+
+
+ m2e.version
+
+
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupAdmin.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupAdmin.java
new file mode 100644
index 000000000000..25055fd5e8e6
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupAdmin.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.util.BackupSet;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * The administrative API for HBase Backup. Construct an instance and call {@link #close()}
+ * afterwards.
+ *
+ * BackupAdmin can be used to create backups, restore data from backups and for other backup-related
+ * operations.
+ * @since 2.0
+ */
+@InterfaceAudience.Private
+public interface BackupAdmin extends Closeable {
+
+ /**
+ * Backup given list of tables fully. This is a synchronous operation. It returns backup id on
+ * success or throw exception on failure.
+ * @param userRequest BackupRequest instance
+ * @return the backup Id
+ */
+
+ String backupTables(final BackupRequest userRequest) throws IOException;
+
+ /**
+ * Restore backup
+ * @param request restore request
+ * @throws IOException exception
+ */
+ void restore(RestoreRequest request) throws IOException;
+
+ /**
+ * Describe backup image command
+ * @param backupId backup id
+ * @return backup info
+ * @throws IOException exception
+ */
+ BackupInfo getBackupInfo(String backupId) throws IOException;
+
+ /**
+ * Delete backup image command
+ * @param backupIds array of backup ids
+ * @return total number of deleted sessions
+ * @throws IOException exception
+ */
+ int deleteBackups(String[] backupIds) throws IOException;
+
+ /**
+ * Merge backup images command
+ * @param backupIds array of backup ids of images to be merged The resulting backup image will
+ * have the same backup id as the most recent image from a list of images to be
+ * merged
+ * @throws IOException exception
+ */
+ void mergeBackups(String[] backupIds) throws IOException;
+
+ /**
+ * Show backup history command
+ * @param n last n backup sessions
+ * @return list of backup info objects
+ * @throws IOException exception
+ */
+ List getHistory(int n) throws IOException;
+
+ /**
+ * Show backup history command with filters
+ * @param n last n backup sessions
+ * @param f list of filters
+ * @return list of backup info objects
+ * @throws IOException exception
+ */
+ List getHistory(int n, BackupInfo.Filter... f) throws IOException;
+
+ /**
+ * Backup sets list command - list all backup sets. Backup set is a named group of tables.
+ * @return all registered backup sets
+ * @throws IOException exception
+ */
+ List listBackupSets() throws IOException;
+
+ /**
+ * Backup set describe command. Shows list of tables in this particular backup set.
+ * @param name set name
+ * @return backup set description or null
+ * @throws IOException exception
+ */
+ BackupSet getBackupSet(String name) throws IOException;
+
+ /**
+ * Delete backup set command
+ * @param name backup set name
+ * @return true, if success, false - otherwise
+ * @throws IOException exception
+ */
+ boolean deleteBackupSet(String name) throws IOException;
+
+ /**
+ * Add tables to backup set command
+ * @param name name of backup set.
+ * @param tables array of tables to be added to this set.
+ * @throws IOException exception
+ */
+ void addToBackupSet(String name, TableName[] tables) throws IOException;
+
+ /**
+ * Remove tables from backup set
+ * @param name name of backup set.
+ * @param tables array of tables to be removed from this set.
+ * @throws IOException exception
+ */
+ void removeFromBackupSet(String name, TableName[] tables) throws IOException;
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupClientFactory.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupClientFactory.java
new file mode 100644
index 000000000000..d710e82c4fd3
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupClientFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.backup.impl.FullTableBackupClient;
+import org.apache.hadoop.hbase.backup.impl.IncrementalTableBackupClient;
+import org.apache.hadoop.hbase.backup.impl.TableBackupClient;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.yetus.audience.InterfaceAudience;
+
+@InterfaceAudience.Private
+public final class BackupClientFactory {
+ private BackupClientFactory() {
+ }
+
+ public static TableBackupClient create(Connection conn, String backupId, BackupRequest request)
+ throws IOException {
+ Configuration conf = conn.getConfiguration();
+ try {
+ String clsName = conf.get(TableBackupClient.BACKUP_CLIENT_IMPL_CLASS);
+ if (clsName != null) {
+ Class extends TableBackupClient> clientImpl;
+ clientImpl = Class.forName(clsName).asSubclass(TableBackupClient.class);
+ TableBackupClient client = clientImpl.getDeclaredConstructor().newInstance();
+ client.init(conn, backupId, request);
+ return client;
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+
+ BackupType type = request.getBackupType();
+ if (type == BackupType.FULL) {
+ return new FullTableBackupClient(conn, backupId, request);
+ } else {
+ return new IncrementalTableBackupClient(conn, backupId, request);
+ }
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupCopyJob.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupCopyJob.java
new file mode 100644
index 000000000000..4753003fdf2b
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupCopyJob.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configurable;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.backup.impl.BackupManager;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Backup copy job is a part of a backup process. The concrete implementation is responsible for
+ * copying data from a cluster to backup destination. Concrete implementation is provided by backup
+ * provider, see {@link BackupRestoreFactory}
+ */
+@InterfaceAudience.Private
+public interface BackupCopyJob extends Configurable {
+ /**
+ * Copy backup data to destination
+ * @param backupInfo context object
+ * @param backupManager backup manager
+ * @param conf configuration
+ * @param backupType backup type (FULL or INCREMENTAL)
+ * @param options array of options (implementation-specific)
+ * @return result (0 - success, -1 failure )
+ * @throws IOException exception
+ */
+ int copy(BackupInfo backupInfo, BackupManager backupManager, Configuration conf,
+ BackupType backupType, String[] options) throws IOException;
+
+ /**
+ * Cancel copy job
+ * @param jobHandler backup copy job handler
+ * @throws IOException if cancelling the jobs fails
+ */
+ void cancel(String jobHandler) throws IOException;
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java
new file mode 100644
index 000000000000..547a39c8d623
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupDriver.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BACKUP_LIST_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_LIST;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_PATH;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_PATH_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_RECORD_NUMBER;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_RECORD_NUMBER_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_WORKERS;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_WORKERS_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME_DESC;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Objects;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.backup.BackupRestoreConstants.BackupCommand;
+import org.apache.hadoop.hbase.backup.impl.BackupCommands;
+import org.apache.hadoop.hbase.backup.impl.BackupManager;
+import org.apache.hadoop.hbase.logging.Log4jUtils;
+import org.apache.hadoop.hbase.util.AbstractHBaseTool;
+import org.apache.hadoop.hbase.util.CommonFSUtils;
+import org.apache.hadoop.util.ToolRunner;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
+
+/**
+ * Command-line entry point for backup operation
+ */
+@InterfaceAudience.Private
+public class BackupDriver extends AbstractHBaseTool {
+
+ private static final Logger LOG = LoggerFactory.getLogger(BackupDriver.class);
+ private CommandLine cmd;
+
+ public BackupDriver() throws IOException {
+ init();
+ }
+
+ protected void init() throws IOException {
+ // disable irrelevant loggers to avoid it mess up command output
+ Log4jUtils.disableZkAndClientLoggers();
+ }
+
+ private int parseAndRun(String[] args) throws IOException {
+
+ // Check if backup is enabled
+ if (!BackupManager.isBackupEnabled(getConf())) {
+ System.err.println(BackupRestoreConstants.ENABLE_BACKUP);
+ return -1;
+ }
+
+ String cmd = null;
+ String[] remainArgs = null;
+ if (args == null || args.length == 0) {
+ printToolUsage();
+ return -1;
+ } else {
+ cmd = args[0];
+ remainArgs = new String[args.length - 1];
+ if (args.length > 1) {
+ System.arraycopy(args, 1, remainArgs, 0, args.length - 1);
+ }
+ }
+
+ BackupCommand type = BackupCommand.HELP;
+ if (BackupCommand.CREATE.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.CREATE;
+ } else if (BackupCommand.HELP.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.HELP;
+ } else if (BackupCommand.DELETE.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.DELETE;
+ } else if (BackupCommand.DESCRIBE.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.DESCRIBE;
+ } else if (BackupCommand.HISTORY.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.HISTORY;
+ } else if (BackupCommand.PROGRESS.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.PROGRESS;
+ } else if (BackupCommand.SET.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.SET;
+ } else if (BackupCommand.REPAIR.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.REPAIR;
+ } else if (BackupCommand.MERGE.name().equalsIgnoreCase(cmd)) {
+ type = BackupCommand.MERGE;
+ } else {
+ System.out.println("Unsupported command for backup: " + cmd);
+ printToolUsage();
+ return -1;
+ }
+
+ // enable debug logging
+ if (this.cmd.hasOption(OPTION_DEBUG)) {
+ Log4jUtils.setLogLevel("org.apache.hadoop.hbase.backup", "DEBUG");
+ }
+
+ BackupCommands.Command command = BackupCommands.createCommand(getConf(), type, this.cmd);
+ if (type == BackupCommand.CREATE && conf != null) {
+ ((BackupCommands.CreateCommand) command).setConf(conf);
+ }
+ try {
+ command.execute();
+ } catch (IOException e) {
+ if (e.getMessage().equals(BackupCommands.INCORRECT_USAGE)) {
+ return -1;
+ }
+ throw e;
+ } finally {
+ command.finish();
+ }
+ return 0;
+ }
+
+ @Override
+ protected void addOptions() {
+ // define supported options
+ addOptNoArg(OPTION_DEBUG, OPTION_DEBUG_DESC);
+ addOptWithArg(OPTION_TABLE, OPTION_TABLE_DESC);
+ addOptWithArg(OPTION_BANDWIDTH, OPTION_BANDWIDTH_DESC);
+ addOptWithArg(OPTION_LIST, OPTION_BACKUP_LIST_DESC);
+ addOptWithArg(OPTION_WORKERS, OPTION_WORKERS_DESC);
+ addOptWithArg(OPTION_RECORD_NUMBER, OPTION_RECORD_NUMBER_DESC);
+ addOptWithArg(OPTION_SET, OPTION_SET_DESC);
+ addOptWithArg(OPTION_PATH, OPTION_PATH_DESC);
+ addOptWithArg(OPTION_KEEP, OPTION_KEEP_DESC);
+ addOptWithArg(OPTION_YARN_QUEUE_NAME, OPTION_YARN_QUEUE_NAME_DESC);
+
+ }
+
+ @Override
+ protected void processOptions(CommandLine cmd) {
+ this.cmd = cmd;
+ }
+
+ @Override
+ protected int doWork() throws Exception {
+ return parseAndRun(cmd.getArgs());
+ }
+
+ public static void main(String[] args) throws Exception {
+ Configuration conf = HBaseConfiguration.create();
+ Path hbasedir = CommonFSUtils.getRootDir(conf);
+ URI defaultFs = hbasedir.getFileSystem(conf).getUri();
+ CommonFSUtils.setFsDefault(conf, new Path(defaultFs));
+ int ret = ToolRunner.run(conf, new BackupDriver(), args);
+ System.exit(ret);
+ }
+
+ @Override
+ public int run(String[] args) throws IOException {
+ Objects.requireNonNull(conf, "Tool configuration is not initialized");
+
+ CommandLine cmd;
+ try {
+ // parse the command line arguments
+ cmd = parseArgs(args);
+ cmdLineArgs = args;
+ } catch (Exception e) {
+ System.err.println("Error when parsing command-line arguments: " + e.getMessage());
+ printToolUsage();
+ return EXIT_FAILURE;
+ }
+ processOptions(cmd);
+
+ int ret = EXIT_FAILURE;
+ try {
+ ret = doWork();
+ } catch (Exception e) {
+ LOG.error("Error running command-line tool", e);
+ return EXIT_FAILURE;
+ }
+ return ret;
+ }
+
+ protected void printToolUsage() throws IOException {
+ System.out.println(BackupCommands.USAGE);
+ System.out.println(BackupRestoreConstants.VERIFY_BACKUP);
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupHFileCleaner.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupHFileCleaner.java
new file mode 100644
index 000000000000..619cecaeaaac
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupHFileCleaner.java
@@ -0,0 +1,180 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.Abortable;
+import org.apache.hadoop.hbase.HBaseInterfaceAudience;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate;
+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.Iterables;
+
+/**
+ * Implementation of a file cleaner that checks if an hfile is still referenced by backup before
+ * deleting it from hfile archive directory.
+ */
+@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
+public class BackupHFileCleaner extends BaseHFileCleanerDelegate implements Abortable {
+ private static final Logger LOG = LoggerFactory.getLogger(BackupHFileCleaner.class);
+ private boolean stopped = false;
+ private boolean aborted;
+ private Configuration conf;
+ private Connection connection;
+ private long prevReadFromBackupTbl = 0, // timestamp of most recent read from backup:system table
+ secondPrevReadFromBackupTbl = 0; // timestamp of 2nd most recent read from backup:system table
+ // used by unit test to skip reading backup:system
+ private boolean checkForFullyBackedUpTables = true;
+ private List fullyBackedUpTables = null;
+
+ private Set getFilenameFromBulkLoad(Map>[] maps) {
+ Set filenames = new HashSet<>();
+ for (Map> map : maps) {
+ if (map == null) {
+ continue;
+ }
+
+ for (List paths : map.values()) {
+ for (Path p : paths) {
+ filenames.add(p.getName());
+ }
+ }
+ }
+ return filenames;
+ }
+
+ private Set loadHFileRefs(List tableList) throws IOException {
+ if (connection == null) {
+ connection = ConnectionFactory.createConnection(conf);
+ }
+ try (BackupSystemTable tbl = new BackupSystemTable(connection)) {
+ Map>[] res = tbl.readBulkLoadedFiles(null, tableList);
+ secondPrevReadFromBackupTbl = prevReadFromBackupTbl;
+ prevReadFromBackupTbl = EnvironmentEdgeManager.currentTime();
+ return getFilenameFromBulkLoad(res);
+ }
+ }
+
+ @InterfaceAudience.Private
+ void setCheckForFullyBackedUpTables(boolean b) {
+ checkForFullyBackedUpTables = b;
+ }
+
+ @Override
+ public Iterable getDeletableFiles(Iterable files) {
+ if (conf == null) {
+ return files;
+ }
+ // obtain the Set of TableName's which have been fully backed up
+ // so that we filter BulkLoad to be returned from server
+ if (checkForFullyBackedUpTables) {
+ if (connection == null) {
+ return files;
+ }
+
+ try (BackupSystemTable tbl = new BackupSystemTable(connection)) {
+ fullyBackedUpTables = tbl.getTablesForBackupType(BackupType.FULL);
+ } catch (IOException ioe) {
+ LOG.error("Failed to get tables which have been fully backed up, skipping checking", ioe);
+ return Collections.emptyList();
+ }
+ Collections.sort(fullyBackedUpTables);
+ }
+ final Set hfileRefs;
+ try {
+ hfileRefs = loadHFileRefs(fullyBackedUpTables);
+ } catch (IOException ioe) {
+ LOG.error("Failed to read hfile references, skipping checking deletable files", ioe);
+ return Collections.emptyList();
+ }
+ Iterable deletables = Iterables.filter(files, file -> {
+ // If the file is recent, be conservative and wait for one more scan of backup:system table
+ if (file.getModificationTime() > secondPrevReadFromBackupTbl) {
+ return false;
+ }
+ String hfile = file.getPath().getName();
+ boolean foundHFileRef = hfileRefs.contains(hfile);
+ return !foundHFileRef;
+ });
+ return deletables;
+ }
+
+ @Override
+ public boolean isFileDeletable(FileStatus fStat) {
+ // work is done in getDeletableFiles()
+ return true;
+ }
+
+ @Override
+ public void setConf(Configuration config) {
+ this.conf = config;
+ this.connection = null;
+ try {
+ this.connection = ConnectionFactory.createConnection(conf);
+ } catch (IOException ioe) {
+ LOG.error("Couldn't establish connection", ioe);
+ }
+ }
+
+ @Override
+ public void stop(String why) {
+ if (this.stopped) {
+ return;
+ }
+ if (this.connection != null) {
+ try {
+ this.connection.close();
+ } catch (IOException ioe) {
+ LOG.debug("Got " + ioe + " when closing connection");
+ }
+ }
+ this.stopped = true;
+ }
+
+ @Override
+ public boolean isStopped() {
+ return this.stopped;
+ }
+
+ @Override
+ public void abort(String why, Throwable e) {
+ LOG.warn("Aborting ReplicationHFileCleaner because " + why, e);
+ this.aborted = true;
+ stop(why);
+ }
+
+ @Override
+ public boolean isAborted() {
+ return this.aborted;
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupInfo.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupInfo.java
new file mode 100644
index 000000000000..fdad0d549830
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupInfo.java
@@ -0,0 +1,578 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.util.BackupUtils;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
+import org.apache.hadoop.hbase.shaded.protobuf.generated.BackupProtos;
+
+/**
+ * An object to encapsulate the information for each backup session
+ */
+@InterfaceAudience.Private
+public class BackupInfo implements Comparable {
+ private static final Logger LOG = LoggerFactory.getLogger(BackupInfo.class);
+
+ public interface Filter {
+ /**
+ * Filter interface
+ * @param info backup info
+ * @return true if info passes filter, false otherwise
+ */
+ boolean apply(BackupInfo info);
+ }
+
+ /**
+ * Backup session states
+ */
+ public enum BackupState {
+ RUNNING,
+ COMPLETE,
+ FAILED,
+ ANY
+ }
+
+ /**
+ * BackupPhase - phases of an ACTIVE backup session (running), when state of a backup session is
+ * BackupState.RUNNING
+ */
+ public enum BackupPhase {
+ REQUEST,
+ SNAPSHOT,
+ PREPARE_INCREMENTAL,
+ SNAPSHOTCOPY,
+ INCREMENTAL_COPY,
+ STORE_MANIFEST
+ }
+
+ /**
+ * Backup id
+ */
+ private String backupId;
+
+ /**
+ * Backup type, full or incremental
+ */
+ private BackupType type;
+
+ /**
+ * Target root directory for storing the backup files
+ */
+ private String backupRootDir;
+
+ /**
+ * Backup state
+ */
+ private BackupState state;
+
+ /**
+ * Backup phase
+ */
+ private BackupPhase phase = BackupPhase.REQUEST;
+
+ /**
+ * Backup failure message
+ */
+ private String failedMsg;
+
+ /**
+ * Backup status map for all tables
+ */
+ private Map backupTableInfoMap;
+
+ /**
+ * Actual start timestamp of a backup process
+ */
+ private long startTs;
+
+ /**
+ * Actual end timestamp of the backup process
+ */
+ private long completeTs;
+
+ /**
+ * Total bytes of incremental logs copied
+ */
+ private long totalBytesCopied;
+
+ /**
+ * For incremental backup, a location of a backed-up hlogs
+ */
+ private String hlogTargetDir = null;
+
+ /**
+ * Incremental backup file list
+ */
+ private List incrBackupFileList;
+
+ /**
+ * New region server log timestamps for table set after distributed log roll key - table name,
+ * value - map of RegionServer hostname -> last log rolled timestamp
+ */
+ private Map> tableSetTimestampMap;
+
+ /**
+ * Previous Region server log timestamps for table set after distributed log roll key - table
+ * name, value - map of RegionServer hostname -> last log rolled timestamp
+ */
+ private Map> incrTimestampMap;
+
+ /**
+ * Backup progress in %% (0-100)
+ */
+ private int progress;
+
+ /**
+ * Number of parallel workers. -1 - system defined
+ */
+ private int workers = -1;
+
+ /**
+ * Bandwidth per worker in MB per sec. -1 - unlimited
+ */
+ private long bandwidth = -1;
+
+ public BackupInfo() {
+ backupTableInfoMap = new HashMap<>();
+ }
+
+ public BackupInfo(String backupId, BackupType type, TableName[] tables, String targetRootDir) {
+ this();
+ this.backupId = backupId;
+ this.type = type;
+ this.backupRootDir = targetRootDir;
+ this.addTables(tables);
+ if (type == BackupType.INCREMENTAL) {
+ setHLogTargetDir(BackupUtils.getLogBackupDir(targetRootDir, backupId));
+ }
+ this.startTs = 0;
+ this.completeTs = 0;
+ }
+
+ public int getWorkers() {
+ return workers;
+ }
+
+ public void setWorkers(int workers) {
+ this.workers = workers;
+ }
+
+ public long getBandwidth() {
+ return bandwidth;
+ }
+
+ public void setBandwidth(long bandwidth) {
+ this.bandwidth = bandwidth;
+ }
+
+ public void setBackupTableInfoMap(Map backupTableInfoMap) {
+ this.backupTableInfoMap = backupTableInfoMap;
+ }
+
+ public Map> getTableSetTimestampMap() {
+ return tableSetTimestampMap;
+ }
+
+ public void setTableSetTimestampMap(Map> tableSetTimestampMap) {
+ this.tableSetTimestampMap = tableSetTimestampMap;
+ }
+
+ public void setType(BackupType type) {
+ this.type = type;
+ }
+
+ public void setBackupRootDir(String targetRootDir) {
+ this.backupRootDir = targetRootDir;
+ }
+
+ public void setTotalBytesCopied(long totalBytesCopied) {
+ this.totalBytesCopied = totalBytesCopied;
+ }
+
+ /**
+ * Set progress (0-100%)
+ * @param p progress value
+ */
+ public void setProgress(int p) {
+ this.progress = p;
+ }
+
+ /**
+ * Get current progress
+ */
+ public int getProgress() {
+ return progress;
+ }
+
+ public String getBackupId() {
+ return backupId;
+ }
+
+ public void setBackupId(String backupId) {
+ this.backupId = backupId;
+ }
+
+ public BackupTableInfo getBackupTableInfo(TableName table) {
+ return this.backupTableInfoMap.get(table);
+ }
+
+ public String getFailedMsg() {
+ return failedMsg;
+ }
+
+ public void setFailedMsg(String failedMsg) {
+ this.failedMsg = failedMsg;
+ }
+
+ public long getStartTs() {
+ return startTs;
+ }
+
+ public void setStartTs(long startTs) {
+ this.startTs = startTs;
+ }
+
+ public long getCompleteTs() {
+ return completeTs;
+ }
+
+ public void setCompleteTs(long endTs) {
+ this.completeTs = endTs;
+ }
+
+ public long getTotalBytesCopied() {
+ return totalBytesCopied;
+ }
+
+ public BackupState getState() {
+ return state;
+ }
+
+ public void setState(BackupState flag) {
+ this.state = flag;
+ }
+
+ public BackupPhase getPhase() {
+ return phase;
+ }
+
+ public void setPhase(BackupPhase phase) {
+ this.phase = phase;
+ }
+
+ public BackupType getType() {
+ return type;
+ }
+
+ public void setSnapshotName(TableName table, String snapshotName) {
+ this.backupTableInfoMap.get(table).setSnapshotName(snapshotName);
+ }
+
+ public String getSnapshotName(TableName table) {
+ return this.backupTableInfoMap.get(table).getSnapshotName();
+ }
+
+ public List getSnapshotNames() {
+ List snapshotNames = new ArrayList<>();
+ for (BackupTableInfo backupStatus : this.backupTableInfoMap.values()) {
+ snapshotNames.add(backupStatus.getSnapshotName());
+ }
+ return snapshotNames;
+ }
+
+ public Set getTables() {
+ return this.backupTableInfoMap.keySet();
+ }
+
+ public List getTableNames() {
+ return new ArrayList<>(backupTableInfoMap.keySet());
+ }
+
+ public void addTables(TableName[] tables) {
+ for (TableName table : tables) {
+ BackupTableInfo backupStatus = new BackupTableInfo(table, this.backupRootDir, this.backupId);
+ this.backupTableInfoMap.put(table, backupStatus);
+ }
+ }
+
+ public void setTables(List tables) {
+ this.backupTableInfoMap.clear();
+ for (TableName table : tables) {
+ BackupTableInfo backupStatus = new BackupTableInfo(table, this.backupRootDir, this.backupId);
+ this.backupTableInfoMap.put(table, backupStatus);
+ }
+ }
+
+ public String getBackupRootDir() {
+ return backupRootDir;
+ }
+
+ public String getTableBackupDir(TableName tableName) {
+ return BackupUtils.getTableBackupDir(backupRootDir, backupId, tableName);
+ }
+
+ public void setHLogTargetDir(String hlogTagetDir) {
+ this.hlogTargetDir = hlogTagetDir;
+ }
+
+ public String getHLogTargetDir() {
+ return hlogTargetDir;
+ }
+
+ public List getIncrBackupFileList() {
+ return incrBackupFileList;
+ }
+
+ public void setIncrBackupFileList(List incrBackupFileList) {
+ this.incrBackupFileList = incrBackupFileList;
+ }
+
+ /**
+ * Set the new region server log timestamps after distributed log roll
+ * @param prevTableSetTimestampMap table timestamp map
+ */
+ public void setIncrTimestampMap(Map> prevTableSetTimestampMap) {
+ this.incrTimestampMap = prevTableSetTimestampMap;
+ }
+
+ /**
+ * Get new region server log timestamps after distributed log roll
+ * @return new region server log timestamps
+ */
+ public Map> getIncrTimestampMap() {
+ return this.incrTimestampMap;
+ }
+
+ public TableName getTableBySnapshot(String snapshotName) {
+ for (Entry entry : this.backupTableInfoMap.entrySet()) {
+ if (snapshotName.equals(entry.getValue().getSnapshotName())) {
+ return entry.getKey();
+ }
+ }
+ return null;
+ }
+
+ public BackupProtos.BackupInfo toProtosBackupInfo() {
+ BackupProtos.BackupInfo.Builder builder = BackupProtos.BackupInfo.newBuilder();
+ builder.setBackupId(getBackupId());
+ setBackupTableInfoMap(builder);
+ setTableSetTimestampMap(builder);
+ builder.setCompleteTs(getCompleteTs());
+ if (getFailedMsg() != null) {
+ builder.setFailedMessage(getFailedMsg());
+ }
+ if (getState() != null) {
+ builder.setBackupState(BackupProtos.BackupInfo.BackupState.valueOf(getState().name()));
+ }
+ if (getPhase() != null) {
+ builder.setBackupPhase(BackupProtos.BackupInfo.BackupPhase.valueOf(getPhase().name()));
+ }
+
+ builder.setProgress(getProgress());
+ builder.setStartTs(getStartTs());
+ builder.setBackupRootDir(getBackupRootDir());
+ builder.setBackupType(BackupProtos.BackupType.valueOf(getType().name()));
+ builder.setWorkersNumber(workers);
+ builder.setBandwidth(bandwidth);
+ return builder.build();
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 33 * type.hashCode() + backupId != null ? backupId.hashCode() : 0;
+ if (backupRootDir != null) {
+ hash = 33 * hash + backupRootDir.hashCode();
+ }
+ hash = 33 * hash + state.hashCode();
+ hash = 33 * hash + phase.hashCode();
+ hash = 33 * hash + (int) (startTs ^ (startTs >>> 32));
+ hash = 33 * hash + (int) (completeTs ^ (completeTs >>> 32));
+ hash = 33 * hash + (int) (totalBytesCopied ^ (totalBytesCopied >>> 32));
+ if (hlogTargetDir != null) {
+ hash = 33 * hash + hlogTargetDir.hashCode();
+ }
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof BackupInfo) {
+ BackupInfo other = (BackupInfo) obj;
+ try {
+ return Bytes.equals(toByteArray(), other.toByteArray());
+ } catch (IOException e) {
+ LOG.error(e.toString(), e);
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return backupId;
+ }
+
+ public byte[] toByteArray() throws IOException {
+ return toProtosBackupInfo().toByteArray();
+ }
+
+ private void setBackupTableInfoMap(BackupProtos.BackupInfo.Builder builder) {
+ for (Entry entry : backupTableInfoMap.entrySet()) {
+ builder.addBackupTableInfo(entry.getValue().toProto());
+ }
+ }
+
+ private void setTableSetTimestampMap(BackupProtos.BackupInfo.Builder builder) {
+ if (this.getTableSetTimestampMap() != null) {
+ for (Entry> entry : this.getTableSetTimestampMap().entrySet()) {
+ builder.putTableSetTimestamp(entry.getKey().getNameAsString(),
+ BackupProtos.BackupInfo.RSTimestampMap.newBuilder().putAllRsTimestamp(entry.getValue())
+ .build());
+ }
+ }
+ }
+
+ public static BackupInfo fromByteArray(byte[] data) throws IOException {
+ return fromProto(BackupProtos.BackupInfo.parseFrom(data));
+ }
+
+ public static BackupInfo fromStream(final InputStream stream) throws IOException {
+ return fromProto(BackupProtos.BackupInfo.parseDelimitedFrom(stream));
+ }
+
+ public static BackupInfo fromProto(BackupProtos.BackupInfo proto) {
+ BackupInfo context = new BackupInfo();
+ context.setBackupId(proto.getBackupId());
+ context.setBackupTableInfoMap(toMap(proto.getBackupTableInfoList()));
+ context.setTableSetTimestampMap(getTableSetTimestampMap(proto.getTableSetTimestampMap()));
+ context.setCompleteTs(proto.getCompleteTs());
+ if (proto.hasFailedMessage()) {
+ context.setFailedMsg(proto.getFailedMessage());
+ }
+ if (proto.hasBackupState()) {
+ context.setState(BackupInfo.BackupState.valueOf(proto.getBackupState().name()));
+ }
+
+ context
+ .setHLogTargetDir(BackupUtils.getLogBackupDir(proto.getBackupRootDir(), proto.getBackupId()));
+
+ if (proto.hasBackupPhase()) {
+ context.setPhase(BackupPhase.valueOf(proto.getBackupPhase().name()));
+ }
+ if (proto.hasProgress()) {
+ context.setProgress(proto.getProgress());
+ }
+ context.setStartTs(proto.getStartTs());
+ context.setBackupRootDir(proto.getBackupRootDir());
+ context.setType(BackupType.valueOf(proto.getBackupType().name()));
+ context.setWorkers(proto.getWorkersNumber());
+ context.setBandwidth(proto.getBandwidth());
+ return context;
+ }
+
+ private static Map toMap(List list) {
+ HashMap map = new HashMap<>();
+ for (BackupProtos.BackupTableInfo tbs : list) {
+ map.put(ProtobufUtil.toTableName(tbs.getTableName()), BackupTableInfo.convert(tbs));
+ }
+ return map;
+ }
+
+ private static Map>
+ getTableSetTimestampMap(Map map) {
+ Map> tableSetTimestampMap = new HashMap<>();
+ for (Entry entry : map.entrySet()) {
+ tableSetTimestampMap.put(TableName.valueOf(entry.getKey()),
+ entry.getValue().getRsTimestampMap());
+ }
+
+ return tableSetTimestampMap;
+ }
+
+ public String getShortDescription() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ sb.append("ID=" + backupId).append(",");
+ sb.append("Type=" + getType()).append(",");
+ sb.append("Tables=" + getTableListAsString()).append(",");
+ sb.append("State=" + getState()).append(",");
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(getStartTs());
+ Date date = cal.getTime();
+ sb.append("Start time=" + date).append(",");
+ if (state == BackupState.FAILED) {
+ sb.append("Failed message=" + getFailedMsg()).append(",");
+ } else if (state == BackupState.RUNNING) {
+ sb.append("Phase=" + getPhase()).append(",");
+ } else if (state == BackupState.COMPLETE) {
+ cal = Calendar.getInstance();
+ cal.setTimeInMillis(getCompleteTs());
+ date = cal.getTime();
+ sb.append("End time=" + date).append(",");
+ }
+ sb.append("Progress=" + getProgress() + "%");
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ public String getStatusAndProgressAsString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("id: ").append(getBackupId()).append(" state: ").append(getState())
+ .append(" progress: ").append(getProgress());
+ return sb.toString();
+ }
+
+ public String getTableListAsString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ sb.append(StringUtils.join(backupTableInfoMap.keySet(), ","));
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * We use only time stamps to compare objects during sort operation
+ */
+ @Override
+ public int compareTo(BackupInfo o) {
+ Long thisTS =
+ Long.valueOf(this.getBackupId().substring(this.getBackupId().lastIndexOf("_") + 1));
+ Long otherTS = Long.valueOf(o.getBackupId().substring(o.getBackupId().lastIndexOf("_") + 1));
+ return thisTS.compareTo(otherTS);
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupMergeJob.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupMergeJob.java
new file mode 100644
index 000000000000..1e2b17145025
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupMergeJob.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configurable;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Backup merge operation job interface. Concrete implementation is provided by backup provider, see
+ * {@link BackupRestoreFactory}
+ */
+
+@InterfaceAudience.Private
+public interface BackupMergeJob extends Configurable {
+ /**
+ * Run backup merge operation.
+ * @param backupIds backup image ids
+ * @throws IOException if the backup merge operation fails
+ */
+ void run(String[] backupIds) throws IOException;
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupObserver.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupObserver.java
new file mode 100644
index 000000000000..73f97365adbe
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupObserver.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseInterfaceAudience;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.impl.BackupManager;
+import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessor;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.coprocessor.RegionObserver;
+import org.apache.hadoop.hbase.util.Pair;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An Observer to facilitate backup operations
+ */
+@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
+public class BackupObserver implements RegionCoprocessor, RegionObserver {
+ private static final Logger LOG = LoggerFactory.getLogger(BackupObserver.class);
+
+ @Override
+ public Optional getRegionObserver() {
+ return Optional.of(this);
+ }
+
+ @Override
+ public void postBulkLoadHFile(ObserverContext ctx,
+ List> stagingFamilyPaths, Map> finalPaths)
+ throws IOException {
+ Configuration cfg = ctx.getEnvironment().getConfiguration();
+ if (finalPaths == null) {
+ // there is no need to record state
+ return;
+ }
+ if (!BackupManager.isBackupEnabled(cfg)) {
+ LOG.debug("skipping recording bulk load in postBulkLoadHFile since backup is disabled");
+ return;
+ }
+ try (Connection connection = ConnectionFactory.createConnection(cfg);
+ BackupSystemTable tbl = new BackupSystemTable(connection)) {
+ List fullyBackedUpTables = tbl.getTablesForBackupType(BackupType.FULL);
+ RegionInfo info = ctx.getEnvironment().getRegionInfo();
+ TableName tableName = info.getTable();
+ if (!fullyBackedUpTables.contains(tableName)) {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace(tableName + " has not gone thru full backup");
+ }
+ return;
+ }
+ tbl.writePathsPostBulkLoad(tableName, info.getEncodedNameAsBytes(), finalPaths);
+ } catch (IOException ioe) {
+ LOG.error("Failed to get tables which have been fully backed up", ioe);
+ }
+ }
+
+ @Override
+ public void preCommitStoreFile(final ObserverContext ctx,
+ final byte[] family, final List> pairs) throws IOException {
+ Configuration cfg = ctx.getEnvironment().getConfiguration();
+ if (pairs == null || pairs.isEmpty() || !BackupManager.isBackupEnabled(cfg)) {
+ LOG.debug("skipping recording bulk load in preCommitStoreFile since backup is disabled");
+ return;
+ }
+ try (Connection connection = ConnectionFactory.createConnection(cfg);
+ BackupSystemTable tbl = new BackupSystemTable(connection)) {
+ List fullyBackedUpTables = tbl.getTablesForBackupType(BackupType.FULL);
+ RegionInfo info = ctx.getEnvironment().getRegionInfo();
+ TableName tableName = info.getTable();
+ if (!fullyBackedUpTables.contains(tableName)) {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace(tableName + " has not gone thru full backup");
+ }
+ return;
+ }
+ tbl.writeFilesForBulkLoadPreCommit(tableName, info.getEncodedNameAsBytes(), family, pairs);
+ return;
+ }
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRequest.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRequest.java
new file mode 100644
index 000000000000..c9c7a5b61810
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRequest.java
@@ -0,0 +1,151 @@
+/*
+ * 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;
+
+import java.util.List;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * POJO class for backup request
+ */
+@InterfaceAudience.Private
+public final class BackupRequest {
+
+ public static class Builder {
+
+ BackupRequest request;
+
+ public Builder() {
+ request = new BackupRequest();
+ }
+
+ public Builder withBackupType(BackupType type) {
+ request.setBackupType(type);
+ return this;
+ }
+
+ public Builder withTableList(List tables) {
+ request.setTableList(tables);
+ return this;
+ }
+
+ public Builder withTargetRootDir(String backupDir) {
+ request.setTargetRootDir(backupDir);
+ return this;
+ }
+
+ public Builder withBackupSetName(String setName) {
+ request.setBackupSetName(setName);
+ return this;
+ }
+
+ public Builder withTotalTasks(int numTasks) {
+ request.setTotalTasks(numTasks);
+ return this;
+ }
+
+ public Builder withBandwidthPerTasks(int bandwidth) {
+ request.setBandwidth(bandwidth);
+ return this;
+ }
+
+ public Builder withYarnPoolName(String name) {
+ request.setYarnPoolName(name);
+ return this;
+ }
+
+ public BackupRequest build() {
+ return request;
+ }
+
+ }
+
+ private BackupType type;
+ private List tableList;
+ private String targetRootDir;
+ private int totalTasks = -1;
+ private long bandwidth = -1L;
+ private String backupSetName;
+ private String yarnPoolName;
+
+ private BackupRequest() {
+ }
+
+ private BackupRequest setBackupType(BackupType type) {
+ this.type = type;
+ return this;
+ }
+
+ public BackupType getBackupType() {
+ return this.type;
+ }
+
+ private BackupRequest setTableList(List tableList) {
+ this.tableList = tableList;
+ return this;
+ }
+
+ public List getTableList() {
+ return this.tableList;
+ }
+
+ private BackupRequest setTargetRootDir(String targetRootDir) {
+ this.targetRootDir = targetRootDir;
+ return this;
+ }
+
+ public String getTargetRootDir() {
+ return this.targetRootDir;
+ }
+
+ private BackupRequest setTotalTasks(int totalTasks) {
+ this.totalTasks = totalTasks;
+ return this;
+ }
+
+ public int getTotalTasks() {
+ return this.totalTasks;
+ }
+
+ private BackupRequest setBandwidth(long bandwidth) {
+ this.bandwidth = bandwidth;
+ return this;
+ }
+
+ public long getBandwidth() {
+ return this.bandwidth;
+ }
+
+ public String getBackupSetName() {
+ return backupSetName;
+ }
+
+ private BackupRequest setBackupSetName(String backupSetName) {
+ this.backupSetName = backupSetName;
+ return this;
+ }
+
+ public String getYarnPoolName() {
+ return yarnPoolName;
+ }
+
+ public void setYarnPoolName(String yarnPoolName) {
+ this.yarnPoolName = yarnPoolName;
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java
new file mode 100644
index 000000000000..56c454519d81
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreConstants.java
@@ -0,0 +1,140 @@
+/*
+ * 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;
+
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * BackupRestoreConstants holds a bunch of HBase Backup and Restore constants
+ */
+@InterfaceAudience.Private
+public interface BackupRestoreConstants {
+ /*
+ * Backup/Restore constants
+ */
+ String BACKUP_SYSTEM_TABLE_NAME_KEY = "hbase.backup.system.table.name";
+ String BACKUP_SYSTEM_TABLE_NAME_DEFAULT = "backup:system";
+
+ String BACKUP_SYSTEM_TTL_KEY = "hbase.backup.system.ttl";
+
+ int BACKUP_SYSTEM_TTL_DEFAULT = HConstants.FOREVER;
+ String BACKUP_ENABLE_KEY = "hbase.backup.enable";
+ boolean BACKUP_ENABLE_DEFAULT = false;
+
+ String BACKUP_MAX_ATTEMPTS_KEY = "hbase.backup.attempts.max";
+ int DEFAULT_BACKUP_MAX_ATTEMPTS = 10;
+
+ String BACKUP_ATTEMPTS_PAUSE_MS_KEY = "hbase.backup.attempts.pause.ms";
+ int DEFAULT_BACKUP_ATTEMPTS_PAUSE_MS = 10000;
+
+ /*
+ * Drivers option list
+ */
+ String OPTION_OVERWRITE = "o";
+ String OPTION_OVERWRITE_DESC = "Overwrite data if any of the restore target tables exists";
+
+ String OPTION_CHECK = "c";
+ String OPTION_CHECK_DESC =
+ "Check restore sequence and dependencies only (does not execute the command)";
+
+ String OPTION_SET = "s";
+ String OPTION_SET_DESC = "Backup set name";
+ String OPTION_SET_RESTORE_DESC = "Backup set to restore, mutually exclusive with -t (table list)";
+ String OPTION_SET_BACKUP_DESC = "Backup set to backup, mutually exclusive with -t (table list)";
+ String OPTION_DEBUG = "d";
+ String OPTION_DEBUG_DESC = "Enable debug loggings";
+
+ String OPTION_TABLE = "t";
+ String OPTION_TABLE_DESC =
+ "Table name. If specified, only backup images," + " which contain this table will be listed.";
+
+ String OPTION_LIST = "l";
+ String OPTION_TABLE_LIST_DESC = "Table name list, comma-separated.";
+ String OPTION_BACKUP_LIST_DESC = "Backup ids list, comma-separated.";
+
+ String OPTION_BANDWIDTH = "b";
+ String OPTION_BANDWIDTH_DESC = "Bandwidth per task (MapReduce task) in MB/s";
+
+ String OPTION_WORKERS = "w";
+ String OPTION_WORKERS_DESC = "Number of parallel MapReduce tasks to execute";
+
+ String OPTION_RECORD_NUMBER = "n";
+ String OPTION_RECORD_NUMBER_DESC = "Number of records of backup history. Default: 10";
+
+ String OPTION_PATH = "p";
+ String OPTION_PATH_DESC = "Backup destination root directory path";
+
+ String OPTION_KEEP = "k";
+ String OPTION_KEEP_DESC = "Specifies maximum age of backup (in days) to keep during bulk delete";
+
+ String OPTION_TABLE_MAPPING = "m";
+ String OPTION_TABLE_MAPPING_DESC = "A comma separated list of target tables. "
+ + "If specified, each table in must have a mapping";
+ String OPTION_YARN_QUEUE_NAME = "q";
+ String OPTION_YARN_QUEUE_NAME_DESC = "Yarn queue name to run backup create command on";
+ String OPTION_YARN_QUEUE_NAME_RESTORE_DESC = "Yarn queue name to run backup restore command on";
+
+ String JOB_NAME_CONF_KEY = "mapreduce.job.name";
+
+ String BACKUP_CONFIG_STRING =
+ BackupRestoreConstants.BACKUP_ENABLE_KEY + "=true\n" + "hbase.master.logcleaner.plugins="
+ + "YOUR_PLUGINS,org.apache.hadoop.hbase.backup.master.BackupLogCleaner\n"
+ + "hbase.procedure.master.classes=YOUR_CLASSES,"
+ + "org.apache.hadoop.hbase.backup.master.LogRollMasterProcedureManager\n"
+ + "hbase.procedure.regionserver.classes=YOUR_CLASSES,"
+ + "org.apache.hadoop.hbase.backup.regionserver.LogRollRegionServerProcedureManager\n"
+ + "hbase.coprocessor.region.classes=YOUR_CLASSES,"
+ + "org.apache.hadoop.hbase.backup.BackupObserver\n" + "and restart the cluster\n"
+ + "For more information please see http://hbase.apache.org/book.html#backuprestore\n";
+ String ENABLE_BACKUP = "Backup is not enabled. To enable backup, " + "in hbase-site.xml, set:\n "
+ + BACKUP_CONFIG_STRING;
+
+ String VERIFY_BACKUP = "To enable backup, in hbase-site.xml, set:\n " + BACKUP_CONFIG_STRING;
+
+ /*
+ * Delimiter in table name list in restore command
+ */
+ String TABLENAME_DELIMITER_IN_COMMAND = ",";
+
+ String CONF_STAGING_ROOT = "snapshot.export.staging.root";
+
+ String BACKUPID_PREFIX = "backup_";
+
+ enum BackupCommand {
+ CREATE,
+ CANCEL,
+ DELETE,
+ DESCRIBE,
+ HISTORY,
+ STATUS,
+ CONVERT,
+ MERGE,
+ STOP,
+ SHOW,
+ HELP,
+ PROGRESS,
+ SET,
+ SET_ADD,
+ SET_REMOVE,
+ SET_DELETE,
+ SET_DESCRIBE,
+ SET_LIST,
+ REPAIR
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreFactory.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreFactory.java
new file mode 100644
index 000000000000..40bbb4bc7fea
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupRestoreFactory.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.backup.mapreduce.MapReduceBackupCopyJob;
+import org.apache.hadoop.hbase.backup.mapreduce.MapReduceBackupMergeJob;
+import org.apache.hadoop.hbase.backup.mapreduce.MapReduceRestoreJob;
+import org.apache.hadoop.util.ReflectionUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Factory implementation for backup/restore related jobs
+ */
+@InterfaceAudience.Private
+public final class BackupRestoreFactory {
+ public final static String HBASE_INCR_RESTORE_IMPL_CLASS = "hbase.incremental.restore.class";
+ public final static String HBASE_BACKUP_COPY_IMPL_CLASS = "hbase.backup.copy.class";
+ public final static String HBASE_BACKUP_MERGE_IMPL_CLASS = "hbase.backup.merge.class";
+
+ private BackupRestoreFactory() {
+ throw new AssertionError("Instantiating utility class...");
+ }
+
+ /**
+ * Gets backup restore job
+ * @param conf configuration
+ * @return backup restore job instance
+ */
+ public static RestoreJob getRestoreJob(Configuration conf) {
+ Class extends RestoreJob> cls =
+ conf.getClass(HBASE_INCR_RESTORE_IMPL_CLASS, MapReduceRestoreJob.class, RestoreJob.class);
+ RestoreJob service = ReflectionUtils.newInstance(cls, conf);
+ service.setConf(conf);
+ return service;
+ }
+
+ /**
+ * Gets backup copy job
+ * @param conf configuration
+ * @return backup copy job instance
+ */
+ public static BackupCopyJob getBackupCopyJob(Configuration conf) {
+ Class extends BackupCopyJob> cls = conf.getClass(HBASE_BACKUP_COPY_IMPL_CLASS,
+ MapReduceBackupCopyJob.class, BackupCopyJob.class);
+ BackupCopyJob service = ReflectionUtils.newInstance(cls, conf);
+ service.setConf(conf);
+ return service;
+ }
+
+ /**
+ * Gets backup merge job
+ * @param conf configuration
+ * @return backup merge job instance
+ */
+ public static BackupMergeJob getBackupMergeJob(Configuration conf) {
+ Class extends BackupMergeJob> cls = conf.getClass(HBASE_BACKUP_MERGE_IMPL_CLASS,
+ MapReduceBackupMergeJob.class, BackupMergeJob.class);
+ BackupMergeJob service = ReflectionUtils.newInstance(cls, conf);
+ service.setConf(conf);
+ return service;
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupTableInfo.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupTableInfo.java
new file mode 100644
index 000000000000..01097422e3a1
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/BackupTableInfo.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import org.apache.hadoop.hbase.TableName;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
+import org.apache.hadoop.hbase.shaded.protobuf.generated.BackupProtos;
+
+/**
+ * Backup related information encapsulated for a table. At this moment only target directory,
+ * snapshot name and table name are encapsulated here.
+ */
+
+@InterfaceAudience.Private
+public class BackupTableInfo {
+ /*
+ * Table name for backup
+ */
+ private TableName table;
+
+ /*
+ * Snapshot name for offline/online snapshot
+ */
+ private String snapshotName = null;
+
+ public BackupTableInfo() {
+ }
+
+ public BackupTableInfo(TableName table, String targetRootDir, String backupId) {
+ this.table = table;
+ }
+
+ public String getSnapshotName() {
+ return snapshotName;
+ }
+
+ public void setSnapshotName(String snapshotName) {
+ this.snapshotName = snapshotName;
+ }
+
+ public TableName getTable() {
+ return table;
+ }
+
+ public static BackupTableInfo convert(BackupProtos.BackupTableInfo proto) {
+ BackupTableInfo bs = new BackupTableInfo();
+ bs.table = ProtobufUtil.toTableName(proto.getTableName());
+ if (proto.hasSnapshotName()) {
+ bs.snapshotName = proto.getSnapshotName();
+ }
+ return bs;
+ }
+
+ public BackupProtos.BackupTableInfo toProto() {
+ BackupProtos.BackupTableInfo.Builder builder = BackupProtos.BackupTableInfo.newBuilder();
+ if (snapshotName != null) {
+ builder.setSnapshotName(snapshotName);
+ }
+ builder.setTableName(ProtobufUtil.toProtoTableName(table));
+ return builder.build();
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/HBackupFileSystem.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/HBackupFileSystem.java
new file mode 100644
index 000000000000..c41a4a182435
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/HBackupFileSystem.java
@@ -0,0 +1,158 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.HashMap;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.impl.BackupManifest;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * View to an on-disk Backup Image FileSytem Provides the set of methods necessary to interact with
+ * the on-disk Backup Image data.
+ */
+@InterfaceAudience.Private
+public final class HBackupFileSystem {
+ public static final Logger LOG = LoggerFactory.getLogger(HBackupFileSystem.class);
+
+ /**
+ * This is utility class.
+ */
+ private HBackupFileSystem() {
+ }
+
+ /**
+ * Given the backup root dir, backup id and the table name, return the backup image location,
+ * which is also where the backup manifest file is. return value look like:
+ * "hdfs://backup.hbase.org:9000/user/biadmin/backup/backup_1396650096738/default/t1_dn/", where
+ * "hdfs://backup.hbase.org:9000/user/biadmin/backup" is a backup root directory
+ * @param backupRootDir backup root directory
+ * @param backupId backup id
+ * @param tableName table name
+ * @return backupPath String for the particular table
+ */
+ public static String getTableBackupDir(String backupRootDir, String backupId,
+ TableName tableName) {
+ return backupRootDir + Path.SEPARATOR + backupId + Path.SEPARATOR
+ + tableName.getNamespaceAsString() + Path.SEPARATOR + tableName.getQualifierAsString()
+ + Path.SEPARATOR;
+ }
+
+ /**
+ * Get backup temporary directory
+ * @param backupRootDir backup root
+ * @return backup tmp directory path
+ */
+ public static Path getBackupTmpDirPath(String backupRootDir) {
+ return new Path(backupRootDir, ".tmp");
+ }
+
+ /**
+ * Get backup tmp directory for backupId
+ * @param backupRoot backup root
+ * @param backupId backup id
+ * @return backup tmp directory path
+ */
+ public static Path getBackupTmpDirPathForBackupId(String backupRoot, String backupId) {
+ return new Path(getBackupTmpDirPath(backupRoot), backupId);
+ }
+
+ public static String getTableBackupDataDir(String backupRootDir, String backupId,
+ TableName tableName) {
+ return getTableBackupDir(backupRootDir, backupId, tableName) + Path.SEPARATOR + "data";
+ }
+
+ public static Path getBackupPath(String backupRootDir, String backupId) {
+ return new Path(backupRootDir + Path.SEPARATOR + backupId);
+ }
+
+ /**
+ * Given the backup root dir, backup id and the table name, return the backup image location,
+ * which is also where the backup manifest file is. return value look like:
+ * "hdfs://backup.hbase.org:9000/user/biadmin/backup/backup_1396650096738/default/t1_dn/", where
+ * "hdfs://backup.hbase.org:9000/user/biadmin/backup" is a backup root directory
+ * @param backupRootPath backup root path
+ * @param tableName table name
+ * @param backupId backup Id
+ * @return backupPath for the particular table
+ */
+ public static Path getTableBackupPath(TableName tableName, Path backupRootPath, String backupId) {
+ return new Path(getTableBackupDir(backupRootPath.toString(), backupId, tableName));
+ }
+
+ /**
+ * Given the backup root dir and the backup id, return the log file location for an incremental
+ * backup.
+ * @param backupRootDir backup root directory
+ * @param backupId backup id
+ * @return logBackupDir: ".../user/biadmin/backup/WALs/backup_1396650096738"
+ */
+ public static String getLogBackupDir(String backupRootDir, String backupId) {
+ return backupRootDir + Path.SEPARATOR + backupId + Path.SEPARATOR
+ + HConstants.HREGION_LOGDIR_NAME;
+ }
+
+ public static Path getLogBackupPath(String backupRootDir, String backupId) {
+ return new Path(getLogBackupDir(backupRootDir, backupId));
+ }
+
+ // TODO we do not keep WAL files anymore
+ // Move manifest file to other place
+ private static Path getManifestPath(Configuration conf, Path backupRootPath, String backupId)
+ throws IOException {
+ FileSystem fs = backupRootPath.getFileSystem(conf);
+ Path manifestPath = new Path(getBackupPath(backupRootPath.toString(), backupId) + Path.SEPARATOR
+ + BackupManifest.MANIFEST_FILE_NAME);
+ if (!fs.exists(manifestPath)) {
+ String errorMsg = "Could not find backup manifest " + BackupManifest.MANIFEST_FILE_NAME
+ + " for " + backupId + ". File " + manifestPath + " does not exists. Did " + backupId
+ + " correspond to previously taken backup ?";
+ throw new IOException(errorMsg);
+ }
+ return manifestPath;
+ }
+
+ public static BackupManifest getManifest(Configuration conf, Path backupRootPath, String backupId)
+ throws IOException {
+ BackupManifest manifest =
+ new BackupManifest(conf, getManifestPath(conf, backupRootPath, backupId));
+ return manifest;
+ }
+
+ /**
+ * Check whether the backup image path and there is manifest file in the path.
+ * @param backupManifestMap If all the manifests are found, then they are put into this map
+ * @param tableArray the tables involved
+ * @throws IOException exception
+ */
+ public static void checkImageManifestExist(HashMap backupManifestMap,
+ TableName[] tableArray, Configuration conf, Path backupRootPath, String backupId)
+ throws IOException {
+ for (TableName tableName : tableArray) {
+ BackupManifest manifest = getManifest(conf, backupRootPath, backupId);
+ backupManifestMap.put(tableName, manifest);
+ }
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreDriver.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreDriver.java
new file mode 100644
index 000000000000..cb01469c8f18
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreDriver.java
@@ -0,0 +1,270 @@
+/*
+ * 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;
+
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_CHECK;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_CHECK_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_OVERWRITE;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_OVERWRITE_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET_RESTORE_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_LIST_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_MAPPING;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_MAPPING_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME_RESTORE_DESC;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl;
+import org.apache.hadoop.hbase.backup.impl.BackupManager;
+import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
+import org.apache.hadoop.hbase.backup.util.BackupUtils;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.logging.Log4jUtils;
+import org.apache.hadoop.hbase.util.AbstractHBaseTool;
+import org.apache.hadoop.hbase.util.CommonFSUtils;
+import org.apache.hadoop.util.ToolRunner;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
+import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter;
+
+/**
+ * Command-line entry point for restore operation
+ */
+@InterfaceAudience.Private
+public class RestoreDriver extends AbstractHBaseTool {
+ private static final Logger LOG = LoggerFactory.getLogger(RestoreDriver.class);
+ private CommandLine cmd;
+
+ private static final String USAGE_STRING =
+ "Usage: hbase restore [options]\n"
+ + " backup_path Path to a backup destination root\n"
+ + " backup_id Backup image ID to restore\n"
+ + " table(s) Comma-separated list of tables to restore\n";
+
+ private static final String USAGE_FOOTER = "";
+
+ protected RestoreDriver() throws IOException {
+ init();
+ }
+
+ protected void init() {
+ // disable irrelevant loggers to avoid it mess up command output
+ Log4jUtils.disableZkAndClientLoggers();
+ }
+
+ private int parseAndRun() throws IOException {
+ // Check if backup is enabled
+ if (!BackupManager.isBackupEnabled(getConf())) {
+ System.err.println(BackupRestoreConstants.ENABLE_BACKUP);
+ return -1;
+ }
+
+ // enable debug logging
+ if (cmd.hasOption(OPTION_DEBUG)) {
+ Log4jUtils.setLogLevel("org.apache.hadoop.hbase.backup", "DEBUG");
+ }
+
+ // whether to overwrite to existing table if any, false by default
+ boolean overwrite = cmd.hasOption(OPTION_OVERWRITE);
+ if (overwrite) {
+ LOG.debug("Found -overwrite option in restore command, "
+ + "will overwrite to existing table if any in the restore target");
+ }
+
+ // whether to only check the dependencies, false by default
+ boolean check = cmd.hasOption(OPTION_CHECK);
+ if (check) {
+ LOG.debug(
+ "Found -check option in restore command, " + "will check and verify the dependencies");
+ }
+
+ if (cmd.hasOption(OPTION_SET) && cmd.hasOption(OPTION_TABLE)) {
+ System.err.println(
+ "Options -s and -t are mutaully exclusive," + " you can not specify both of them.");
+ printToolUsage();
+ return -1;
+ }
+
+ if (!cmd.hasOption(OPTION_SET) && !cmd.hasOption(OPTION_TABLE)) {
+ System.err.println("You have to specify either set name or table list to restore");
+ printToolUsage();
+ return -1;
+ }
+
+ if (cmd.hasOption(OPTION_YARN_QUEUE_NAME)) {
+ String queueName = cmd.getOptionValue(OPTION_YARN_QUEUE_NAME);
+ // Set system property value for MR job
+ System.setProperty("mapreduce.job.queuename", queueName);
+ }
+
+ // parse main restore command options
+ String[] remainArgs = cmd.getArgs();
+ if (remainArgs.length != 2) {
+ printToolUsage();
+ return -1;
+ }
+
+ String backupRootDir = remainArgs[0];
+ String backupId = remainArgs[1];
+ String tables;
+ String tableMapping =
+ cmd.hasOption(OPTION_TABLE_MAPPING) ? cmd.getOptionValue(OPTION_TABLE_MAPPING) : null;
+ try (final Connection conn = ConnectionFactory.createConnection(conf);
+ BackupAdmin client = new BackupAdminImpl(conn)) {
+ // Check backup set
+ if (cmd.hasOption(OPTION_SET)) {
+ String setName = cmd.getOptionValue(OPTION_SET);
+ try {
+ tables = getTablesForSet(conn, setName);
+ } catch (IOException e) {
+ System.out.println("ERROR: " + e.getMessage() + " for setName=" + setName);
+ printToolUsage();
+ return -2;
+ }
+ if (tables == null) {
+ System.out
+ .println("ERROR: Backup set '" + setName + "' is either empty or does not exist");
+ printToolUsage();
+ return -3;
+ }
+ } else {
+ tables = cmd.getOptionValue(OPTION_TABLE);
+ }
+
+ TableName[] sTableArray = BackupUtils.parseTableNames(tables);
+ TableName[] tTableArray = BackupUtils.parseTableNames(tableMapping);
+
+ if (
+ sTableArray != null && tTableArray != null && (sTableArray.length != tTableArray.length)
+ ) {
+ System.out.println("ERROR: table mapping mismatch: " + tables + " : " + tableMapping);
+ printToolUsage();
+ return -4;
+ }
+
+ client.restore(BackupUtils.createRestoreRequest(backupRootDir, backupId, check, sTableArray,
+ tTableArray, overwrite));
+ } catch (Exception e) {
+ LOG.error("Error while running restore backup", e);
+ return -5;
+ }
+ return 0;
+ }
+
+ private String getTablesForSet(Connection conn, String name) throws IOException {
+ try (final BackupSystemTable table = new BackupSystemTable(conn)) {
+ List tables = table.describeBackupSet(name);
+
+ if (tables == null) {
+ return null;
+ }
+
+ return StringUtils.join(tables, BackupRestoreConstants.TABLENAME_DELIMITER_IN_COMMAND);
+ }
+ }
+
+ @Override
+ protected void addOptions() {
+ // define supported options
+ addOptNoArg(OPTION_OVERWRITE, OPTION_OVERWRITE_DESC);
+ addOptNoArg(OPTION_CHECK, OPTION_CHECK_DESC);
+ addOptNoArg(OPTION_DEBUG, OPTION_DEBUG_DESC);
+ addOptWithArg(OPTION_SET, OPTION_SET_RESTORE_DESC);
+ addOptWithArg(OPTION_TABLE, OPTION_TABLE_LIST_DESC);
+ addOptWithArg(OPTION_TABLE_MAPPING, OPTION_TABLE_MAPPING_DESC);
+ addOptWithArg(OPTION_YARN_QUEUE_NAME, OPTION_YARN_QUEUE_NAME_RESTORE_DESC);
+ }
+
+ @Override
+ protected void processOptions(CommandLine cmd) {
+ this.cmd = cmd;
+ }
+
+ @Override
+ protected int doWork() throws Exception {
+ return parseAndRun();
+ }
+
+ public static void main(String[] args) throws Exception {
+ Configuration conf = HBaseConfiguration.create();
+ Path hbasedir = CommonFSUtils.getRootDir(conf);
+ URI defaultFs = hbasedir.getFileSystem(conf).getUri();
+ CommonFSUtils.setFsDefault(conf, new Path(defaultFs));
+ int ret = ToolRunner.run(conf, new RestoreDriver(), args);
+ System.exit(ret);
+ }
+
+ @Override
+ public int run(String[] args) {
+ Objects.requireNonNull(conf, "Tool configuration is not initialized");
+
+ CommandLine cmd;
+ try {
+ // parse the command line arguments
+ cmd = parseArgs(args);
+ cmdLineArgs = args;
+ } catch (Exception e) {
+ System.out.println("Error when parsing command-line arguments: " + e.getMessage());
+ printToolUsage();
+ return EXIT_FAILURE;
+ }
+
+ if (cmd.hasOption(SHORT_HELP_OPTION) || cmd.hasOption(LONG_HELP_OPTION)) {
+ printToolUsage();
+ return EXIT_FAILURE;
+ }
+
+ processOptions(cmd);
+
+ int ret = EXIT_FAILURE;
+ try {
+ ret = doWork();
+ } catch (Exception e) {
+ LOG.error("Error running command-line tool", e);
+ return EXIT_FAILURE;
+ }
+ return ret;
+ }
+
+ protected void printToolUsage() {
+ System.out.println(USAGE_STRING);
+ HelpFormatter helpFormatter = new HelpFormatter();
+ helpFormatter.setLeftPadding(2);
+ helpFormatter.setDescPadding(8);
+ helpFormatter.setWidth(100);
+ helpFormatter.setSyntaxPrefix("Options:");
+ helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
+ System.out.println(BackupRestoreConstants.VERIFY_BACKUP);
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreJob.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreJob.java
new file mode 100644
index 000000000000..b014e6693bbc
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreJob.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configurable;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Restore operation job interface Concrete implementation is provided by backup provider, see
+ * {@link BackupRestoreFactory}
+ */
+
+@InterfaceAudience.Private
+public interface RestoreJob extends Configurable {
+ /**
+ * Run restore operation
+ * @param dirPaths path array of WAL log directories
+ * @param fromTables from tables
+ * @param toTables to tables
+ * @param fullBackupRestore full backup restore
+ * @throws IOException if running the job fails
+ */
+ void run(Path[] dirPaths, TableName[] fromTables, TableName[] toTables, boolean fullBackupRestore)
+ throws IOException;
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreRequest.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreRequest.java
new file mode 100644
index 000000000000..eb4786f57869
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/RestoreRequest.java
@@ -0,0 +1,133 @@
+/*
+ * 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;
+
+import org.apache.hadoop.hbase.TableName;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * POJO class for restore request
+ */
+@InterfaceAudience.Private
+public final class RestoreRequest {
+ public static class Builder {
+ RestoreRequest request;
+
+ public Builder() {
+ request = new RestoreRequest();
+ }
+
+ public Builder withBackupRootDir(String backupRootDir) {
+ request.setBackupRootDir(backupRootDir);
+ return this;
+ }
+
+ public Builder withBackupId(String backupId) {
+ request.setBackupId(backupId);
+ return this;
+ }
+
+ public Builder withCheck(boolean check) {
+ request.setCheck(check);
+ return this;
+ }
+
+ public Builder withFromTables(TableName[] fromTables) {
+ request.setFromTables(fromTables);
+ return this;
+ }
+
+ public Builder withToTables(TableName[] toTables) {
+ request.setToTables(toTables);
+ return this;
+ }
+
+ public Builder withOvewrite(boolean overwrite) {
+ request.setOverwrite(overwrite);
+ return this;
+ }
+
+ public RestoreRequest build() {
+ return request;
+ }
+ }
+
+ private String backupRootDir;
+ private String backupId;
+ private boolean check = false;
+ private TableName[] fromTables;
+ private TableName[] toTables;
+ private boolean overwrite = false;
+
+ private RestoreRequest() {
+ }
+
+ public String getBackupRootDir() {
+ return backupRootDir;
+ }
+
+ private RestoreRequest setBackupRootDir(String backupRootDir) {
+ this.backupRootDir = backupRootDir;
+ return this;
+ }
+
+ public String getBackupId() {
+ return backupId;
+ }
+
+ private RestoreRequest setBackupId(String backupId) {
+ this.backupId = backupId;
+ return this;
+ }
+
+ public boolean isCheck() {
+ return check;
+ }
+
+ private RestoreRequest setCheck(boolean check) {
+ this.check = check;
+ return this;
+ }
+
+ public TableName[] getFromTables() {
+ return fromTables;
+ }
+
+ private RestoreRequest setFromTables(TableName[] fromTables) {
+ this.fromTables = fromTables;
+ return this;
+ }
+
+ public TableName[] getToTables() {
+ return toTables;
+ }
+
+ private RestoreRequest setToTables(TableName[] toTables) {
+ this.toTables = toTables;
+ return this;
+ }
+
+ public boolean isOverwrite() {
+ return overwrite;
+ }
+
+ private RestoreRequest setOverwrite(boolean overwrite) {
+ this.overwrite = overwrite;
+ return this;
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupAdminImpl.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupAdminImpl.java
new file mode 100644
index 000000000000..f580fb0c47bb
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupAdminImpl.java
@@ -0,0 +1,716 @@
+/*
+ * 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 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> 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 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 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}]
+ * @param table backup system table
+ * @throws IOException if a table operation fails
+ */
+ private void finalizeDelete(Map> tablesMap, BackupSystemTable table)
+ throws IOException {
+ for (String backupRoot : tablesMap.keySet()) {
+ Set incrTableSet = table.getIncrementalBackupTableSet(backupRoot);
+ Map> tableMap =
+ table.getBackupHistoryForTableSet(incrTableSet, backupRoot);
+ for (Map.Entry> 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
+ * Algorithm:
+ * Backup type: FULL or INCREMENTAL
+ * Is this last backup session for table T: YES or NO
+ * For every table T from table list 'tables':
+ * if(FULL, YES) deletes only physical data (PD)
+ * if(FULL, NO), deletes PD, scans all newer backups and removes T from backupInfo,
+ * until we either reach the most recent backup for T in the system or FULL backup
+ * which includes T
+ * if(INCREMENTAL, YES) deletes only physical data (PD) if(INCREMENTAL, NO) deletes physical data
+ * and for table T scans all backup images between last
+ * FULL backup, which is older than the backup being deleted and the next FULL backup (if exists)
+ *
+ * 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 tables = backupInfo.getTableNames();
+ long startTime = backupInfo.getStartTs();
+ for (TableName tn : tables) {
+ boolean isLastBackupSession = isLastBackupSession(sysTable, tn, startTime);
+ if (isLastBackupSession) {
+ continue;
+ }
+ // else
+ List affectedBackups = getAffectedBackupSessions(backupInfo, tn, sysTable);
+ for (BackupInfo info : affectedBackups) {
+ if (info.equals(backupInfo)) {
+ continue;
+ }
+ removeTableFromBackupImage(info, tn, sysTable);
+ }
+ }
+ Map 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 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 getAffectedBackupSessions(BackupInfo backupInfo, TableName tn,
+ BackupSystemTable table) throws IOException {
+ LOG.debug("GetAffectedBackupInfos for: " + backupInfo.getBackupId() + " table=" + tn);
+ long ts = backupInfo.getStartTs();
+ List list = new ArrayList<>();
+ List 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 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 history = table.getBackupHistory();
+ for (BackupInfo info : history) {
+ List tables = info.getTableNames();
+ if (!tables.contains(tn)) {
+ continue;
+ }
+ return info.getStartTs() <= startTime;
+ }
+ return false;
+ }
+
+ @Override
+ public List getHistory(int n) throws IOException {
+ try (final BackupSystemTable table = new BackupSystemTable(conn)) {
+ List history = table.getBackupHistory();
+
+ if (history.size() <= n) {
+ return history;
+ }
+
+ List list = new ArrayList<>();
+ for (int i = 0; i < n; i++) {
+ list.add(history.get(i));
+ }
+ return list;
+ }
+ }
+
+ @Override
+ public List getHistory(int n, BackupInfo.Filter... filters) throws IOException {
+ if (filters.length == 0) {
+ return getHistory(n);
+ }
+
+ try (final BackupSystemTable table = new BackupSystemTable(conn)) {
+ List history = table.getBackupHistory();
+ List 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 listBackupSets() throws IOException {
+ try (final BackupSystemTable table = new BackupSystemTable(conn)) {
+ List list = table.listBackupSets();
+ List bslist = new ArrayList<>();
+ for (String s : list) {
+ List 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 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 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 tableList = request.getTableList();
+
+ String backupId = BackupRestoreConstants.BACKUPID_PREFIX + EnvironmentEdgeManager.currentTime();
+ if (type == BackupType.INCREMENTAL) {
+ Set 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 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 excludeNonExistingTables(List tableList,
+ List 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.
+ *
+ * - All backups MUST be in the same destination
+ *
- No FULL backups are allowed - only INCREMENTAL
+ *
- All backups must be in COMPLETE state
+ *
- No holes in backup list are allowed
+ *
+ *
+ * @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 allTables = new HashSet<>();
+ final Set 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 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 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 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);
+ }
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java
new file mode 100644
index 000000000000..ce9c5bbe8fae
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupCommands.java
@@ -0,0 +1,1123 @@
+/*
+ * 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 static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BACKUP_LIST_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_LIST;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_PATH;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_PATH_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_RECORD_NUMBER;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_RECORD_NUMBER_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET_BACKUP_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_LIST_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_WORKERS;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_WORKERS_DESC;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME;
+import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME_DESC;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.conf.Configured;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.BackupAdmin;
+import org.apache.hadoop.hbase.backup.BackupInfo;
+import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
+import org.apache.hadoop.hbase.backup.BackupRequest;
+import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
+import org.apache.hadoop.hbase.backup.BackupRestoreConstants.BackupCommand;
+import org.apache.hadoop.hbase.backup.BackupType;
+import org.apache.hadoop.hbase.backup.HBackupFileSystem;
+import org.apache.hadoop.hbase.backup.util.BackupSet;
+import org.apache.hadoop.hbase.backup.util.BackupUtils;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import org.apache.hbase.thirdparty.com.google.common.base.Splitter;
+import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
+import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
+import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter;
+import org.apache.hbase.thirdparty.org.apache.commons.cli.Options;
+
+/**
+ * General backup commands, options and usage messages
+ */
+@InterfaceAudience.Private
+public final class BackupCommands {
+ public final static String INCORRECT_USAGE = "Incorrect usage";
+
+ public final static String TOP_LEVEL_NOT_ALLOWED =
+ "Top level (root) folder is not allowed to be a backup destination";
+
+ public static final String USAGE = "Usage: hbase backup COMMAND [command-specific arguments]\n"
+ + "where COMMAND is one of:\n" + " create create a new backup image\n"
+ + " delete delete an existing backup image\n"
+ + " describe show the detailed information of a backup image\n"
+ + " history show history of all successful backups\n"
+ + " progress show the progress of the latest backup request\n"
+ + " set backup set management\n" + " repair repair backup system table\n"
+ + " merge merge backup images\n"
+ + "Run \'hbase backup COMMAND -h\' to see help message for each command\n";
+
+ public static final String CREATE_CMD_USAGE =
+ "Usage: hbase backup create [options]\n"
+ + " type \"full\" to create a full backup image\n"
+ + " \"incremental\" to create an incremental backup image\n"
+ + " backup_path Full path to store the backup image\n";
+
+ public static final String PROGRESS_CMD_USAGE = "Usage: hbase backup progress \n"
+ + " backup_id Backup image id (optional). If no id specified, the command will show\n"
+ + " progress for currently running backup session.";
+ public static final String NO_INFO_FOUND = "No info was found for backup id: ";
+ public static final String NO_ACTIVE_SESSION_FOUND = "No active backup sessions found.";
+
+ public static final String DESCRIBE_CMD_USAGE =
+ "Usage: hbase backup describe \n" + " backup_id Backup image id\n";
+
+ public static final String HISTORY_CMD_USAGE = "Usage: hbase backup history [options]";
+
+ public static final String DELETE_CMD_USAGE = "Usage: hbase backup delete [options]";
+
+ public static final String REPAIR_CMD_USAGE = "Usage: hbase backup repair\n";
+
+ public static final String SET_CMD_USAGE = "Usage: hbase backup set COMMAND [name] [tables]\n"
+ + " name Backup set name\n" + " tables Comma separated list of tables.\n"
+ + "COMMAND is one of:\n" + " add add tables to a set, create a set if needed\n"
+ + " remove remove tables from a set\n"
+ + " list list all backup sets in the system\n" + " describe describe set\n"
+ + " delete delete backup set\n";
+ public static final String MERGE_CMD_USAGE = "Usage: hbase backup merge [backup_ids]\n"
+ + " backup_ids Comma separated list of backup image ids.\n";
+
+ public static final String USAGE_FOOTER = "";
+
+ public static abstract class Command extends Configured {
+ CommandLine cmdline;
+ Connection conn;
+
+ Command(Configuration conf) {
+ if (conf == null) {
+ conf = HBaseConfiguration.create();
+ }
+ setConf(conf);
+ }
+
+ public void execute() throws IOException {
+ if (cmdline.hasOption("h") || cmdline.hasOption("help")) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ // Create connection
+ conn = ConnectionFactory.createConnection(getConf());
+ if (requiresNoActiveSession()) {
+ // Check active session
+ try (BackupSystemTable table = new BackupSystemTable(conn)) {
+ List sessions = table.getBackupInfos(BackupState.RUNNING);
+
+ if (sessions.size() > 0) {
+ System.err.println("Found backup session in a RUNNING state: ");
+ System.err.println(sessions.get(0));
+ System.err.println("This may indicate that a previous session has failed abnormally.");
+ System.err.println("In this case, backup recovery is recommended.");
+ throw new IOException("Active session found, aborted command execution");
+ }
+ }
+ }
+ if (requiresConsistentState()) {
+ // Check failed delete
+ try (BackupSystemTable table = new BackupSystemTable(conn)) {
+ String[] ids = table.getListOfBackupIdsFromDeleteOperation();
+
+ if (ids != null && ids.length > 0) {
+ System.err.println("Found failed backup DELETE coommand. ");
+ System.err.println("Backup system recovery is required.");
+ throw new IOException("Failed backup DELETE found, aborted command execution");
+ }
+
+ ids = table.getListOfBackupIdsFromMergeOperation();
+ if (ids != null && ids.length > 0) {
+ System.err.println("Found failed backup MERGE coommand. ");
+ System.err.println("Backup system recovery is required.");
+ throw new IOException("Failed backup MERGE found, aborted command execution");
+ }
+ }
+ }
+ }
+
+ public void finish() throws IOException {
+ if (conn != null) {
+ conn.close();
+ }
+ }
+
+ protected abstract void printUsage();
+
+ /**
+ * The command can't be run if active backup session is in progress
+ * @return true if no active sessions are in progress
+ */
+ protected boolean requiresNoActiveSession() {
+ return false;
+ }
+
+ /**
+ * Command requires consistent state of a backup system Backup system may become inconsistent
+ * because of an abnormal termination of a backup session or delete command
+ * @return true, if yes
+ */
+ protected boolean requiresConsistentState() {
+ return false;
+ }
+ }
+
+ private BackupCommands() {
+ throw new AssertionError("Instantiating utility class...");
+ }
+
+ public static Command createCommand(Configuration conf, BackupCommand type, CommandLine cmdline) {
+ Command cmd;
+ switch (type) {
+ case CREATE:
+ cmd = new CreateCommand(conf, cmdline);
+ break;
+ case DESCRIBE:
+ cmd = new DescribeCommand(conf, cmdline);
+ break;
+ case PROGRESS:
+ cmd = new ProgressCommand(conf, cmdline);
+ break;
+ case DELETE:
+ cmd = new DeleteCommand(conf, cmdline);
+ break;
+ case HISTORY:
+ cmd = new HistoryCommand(conf, cmdline);
+ break;
+ case SET:
+ cmd = new BackupSetCommand(conf, cmdline);
+ break;
+ case REPAIR:
+ cmd = new RepairCommand(conf, cmdline);
+ break;
+ case MERGE:
+ cmd = new MergeCommand(conf, cmdline);
+ break;
+ case HELP:
+ default:
+ cmd = new HelpCommand(conf, cmdline);
+ break;
+ }
+ return cmd;
+ }
+
+ static int numOfArgs(String[] args) {
+ if (args == null) {
+ return 0;
+ }
+
+ return args.length;
+ }
+
+ public static class CreateCommand extends Command {
+ CreateCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ protected boolean requiresNoActiveSession() {
+ return true;
+ }
+
+ @Override
+ protected boolean requiresConsistentState() {
+ return true;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ if (cmdline == null || cmdline.getArgs() == null) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ String[] args = cmdline.getArgs();
+ if (args.length != 3) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ if (
+ !BackupType.FULL.toString().equalsIgnoreCase(args[1])
+ && !BackupType.INCREMENTAL.toString().equalsIgnoreCase(args[1])
+ ) {
+ System.out.println("ERROR: invalid backup type: " + args[1]);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ if (!verifyPath(args[2])) {
+ System.out.println("ERROR: invalid backup destination: " + args[2]);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ String targetBackupDir = args[2];
+ // Check if backup destination is top level (root) folder - not allowed
+ if (isRootFolder(targetBackupDir)) {
+ throw new IOException(TOP_LEVEL_NOT_ALLOWED);
+ }
+ String tables;
+
+ // Check if we have both: backup set and list of tables
+ if (cmdline.hasOption(OPTION_TABLE) && cmdline.hasOption(OPTION_SET)) {
+ System.out
+ .println("ERROR: You can specify either backup set or list" + " of tables, but not both");
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ // Creates connection
+ super.execute();
+ // Check backup set
+ String setName = null;
+ if (cmdline.hasOption(OPTION_SET)) {
+ setName = cmdline.getOptionValue(OPTION_SET);
+ tables = getTablesForSet(setName);
+
+ if (tables == null) {
+ System.out
+ .println("ERROR: Backup set '" + setName + "' is either empty or does not exist");
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ } else {
+ tables = cmdline.getOptionValue(OPTION_TABLE);
+ }
+ int bandwidth = cmdline.hasOption(OPTION_BANDWIDTH)
+ ? Integer.parseInt(cmdline.getOptionValue(OPTION_BANDWIDTH))
+ : -1;
+ int workers = cmdline.hasOption(OPTION_WORKERS)
+ ? Integer.parseInt(cmdline.getOptionValue(OPTION_WORKERS))
+ : -1;
+
+ if (cmdline.hasOption(OPTION_YARN_QUEUE_NAME)) {
+ String queueName = cmdline.getOptionValue(OPTION_YARN_QUEUE_NAME);
+ // Set system property value for MR job
+ System.setProperty("mapreduce.job.queuename", queueName);
+ }
+
+ try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ BackupRequest.Builder builder = new BackupRequest.Builder();
+ BackupRequest request = builder.withBackupType(BackupType.valueOf(args[1].toUpperCase()))
+ .withTableList(
+ tables != null ? Lists.newArrayList(BackupUtils.parseTableNames(tables)) : null)
+ .withTargetRootDir(targetBackupDir).withTotalTasks(workers)
+ .withBandwidthPerTasks(bandwidth).withBackupSetName(setName).build();
+ String backupId = admin.backupTables(request);
+ System.out.println("Backup session " + backupId + " finished. Status: SUCCESS");
+ } catch (IOException e) {
+ System.out.println("Backup session finished. Status: FAILURE");
+ throw e;
+ }
+ }
+
+ private boolean isRootFolder(String targetBackupDir) {
+ Path p = new Path(targetBackupDir);
+ return p.isRoot();
+ }
+
+ private boolean verifyPath(String path) {
+ try {
+ Path p = new Path(path);
+ Configuration conf = getConf() != null ? getConf() : HBaseConfiguration.create();
+ URI uri = p.toUri();
+
+ if (uri.getScheme() == null) {
+ return false;
+ }
+
+ FileSystem.get(uri, conf);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private String getTablesForSet(String name) throws IOException {
+ try (final BackupSystemTable table = new BackupSystemTable(conn)) {
+ List tables = table.describeBackupSet(name);
+
+ if (tables == null) {
+ return null;
+ }
+
+ return StringUtils.join(tables, BackupRestoreConstants.TABLENAME_DELIMITER_IN_COMMAND);
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(CREATE_CMD_USAGE);
+ Options options = new Options();
+ options.addOption(OPTION_WORKERS, true, OPTION_WORKERS_DESC);
+ options.addOption(OPTION_BANDWIDTH, true, OPTION_BANDWIDTH_DESC);
+ options.addOption(OPTION_SET, true, OPTION_SET_BACKUP_DESC);
+ options.addOption(OPTION_TABLE, true, OPTION_TABLE_LIST_DESC);
+ options.addOption(OPTION_YARN_QUEUE_NAME, true, OPTION_YARN_QUEUE_NAME_DESC);
+ options.addOption(OPTION_DEBUG, false, OPTION_DEBUG_DESC);
+
+ HelpFormatter helpFormatter = new HelpFormatter();
+ helpFormatter.setLeftPadding(2);
+ helpFormatter.setDescPadding(8);
+ helpFormatter.setWidth(100);
+ helpFormatter.setSyntaxPrefix("Options:");
+ helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
+ }
+ }
+
+ public static class HelpCommand extends Command {
+ HelpCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ if (cmdline == null) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ String[] args = cmdline.getArgs();
+ if (args == null || args.length == 0) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ if (args.length != 2) {
+ System.out.println("ERROR: Only supports help message of a single command type");
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ String type = args[1];
+
+ if (BackupCommand.CREATE.name().equalsIgnoreCase(type)) {
+ System.out.println(CREATE_CMD_USAGE);
+ } else if (BackupCommand.DESCRIBE.name().equalsIgnoreCase(type)) {
+ System.out.println(DESCRIBE_CMD_USAGE);
+ } else if (BackupCommand.HISTORY.name().equalsIgnoreCase(type)) {
+ System.out.println(HISTORY_CMD_USAGE);
+ } else if (BackupCommand.PROGRESS.name().equalsIgnoreCase(type)) {
+ System.out.println(PROGRESS_CMD_USAGE);
+ } else if (BackupCommand.DELETE.name().equalsIgnoreCase(type)) {
+ System.out.println(DELETE_CMD_USAGE);
+ } else if (BackupCommand.SET.name().equalsIgnoreCase(type)) {
+ System.out.println(SET_CMD_USAGE);
+ } else {
+ System.out.println("Unknown command : " + type);
+ printUsage();
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(USAGE);
+ }
+ }
+
+ public static class DescribeCommand extends Command {
+ DescribeCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ if (cmdline == null || cmdline.getArgs() == null) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ String[] args = cmdline.getArgs();
+ if (args.length != 2) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ super.execute();
+
+ String backupId = args[1];
+ try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
+ BackupInfo info = sysTable.readBackupInfo(backupId);
+ if (info == null) {
+ System.out.println("ERROR: " + backupId + " does not exist");
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ System.out.println(info.getShortDescription());
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(DESCRIBE_CMD_USAGE);
+ }
+ }
+
+ public static class ProgressCommand extends Command {
+ ProgressCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ public void execute() throws IOException {
+
+ if (cmdline == null || cmdline.getArgs() == null || cmdline.getArgs().length == 1) {
+ System.out.println(
+ "No backup id was specified, " + "will retrieve the most recent (ongoing) session");
+ }
+ String[] args = cmdline == null ? null : cmdline.getArgs();
+ if (args != null && args.length > 2) {
+ System.err.println("ERROR: wrong number of arguments: " + args.length);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ super.execute();
+
+ String backupId = (args == null || args.length <= 1) ? null : args[1];
+ try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
+ BackupInfo info = null;
+
+ if (backupId != null) {
+ info = sysTable.readBackupInfo(backupId);
+ } else {
+ List infos = sysTable.getBackupInfos(BackupState.RUNNING);
+ if (infos != null && infos.size() > 0) {
+ info = infos.get(0);
+ backupId = info.getBackupId();
+ System.out.println("Found ongoing session with backupId=" + backupId);
+ }
+ }
+ int progress = info == null ? -1 : info.getProgress();
+ if (progress < 0) {
+ if (backupId != null) {
+ System.out.println(NO_INFO_FOUND + backupId);
+ } else {
+ System.err.println(NO_ACTIVE_SESSION_FOUND);
+ }
+ } else {
+ System.out.println(backupId + " progress=" + progress + "%");
+ }
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(PROGRESS_CMD_USAGE);
+ }
+ }
+
+ public static class DeleteCommand extends Command {
+ DeleteCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ protected boolean requiresNoActiveSession() {
+ return true;
+ }
+
+ @Override
+ public void execute() throws IOException {
+
+ if (cmdline == null || cmdline.getArgs() == null || cmdline.getArgs().length < 1) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ if (!cmdline.hasOption(OPTION_KEEP) && !cmdline.hasOption(OPTION_LIST)) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ super.execute();
+ if (cmdline.hasOption(OPTION_KEEP)) {
+ executeDeleteOlderThan(cmdline);
+ } else if (cmdline.hasOption(OPTION_LIST)) {
+ executeDeleteListOfBackups(cmdline);
+ }
+ }
+
+ private void executeDeleteOlderThan(CommandLine cmdline) throws IOException {
+ String value = cmdline.getOptionValue(OPTION_KEEP);
+ int days = 0;
+ try {
+ days = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new IOException(value + " is not an integer number");
+ }
+ final long fdays = days;
+ BackupInfo.Filter dateFilter = new BackupInfo.Filter() {
+ @Override
+ public boolean apply(BackupInfo info) {
+ long currentTime = EnvironmentEdgeManager.currentTime();
+ long maxTsToDelete = currentTime - fdays * 24 * 3600 * 1000;
+ return info.getCompleteTs() <= maxTsToDelete;
+ }
+ };
+ List history = null;
+ try (final BackupSystemTable sysTable = new BackupSystemTable(conn);
+ BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ history = sysTable.getBackupHistory(-1, dateFilter);
+ String[] backupIds = convertToBackupIds(history);
+ int deleted = admin.deleteBackups(backupIds);
+ System.out.println("Deleted " + deleted + " backups. Total older than " + days + " days: "
+ + backupIds.length);
+ } catch (IOException e) {
+ System.err.println("Delete command FAILED. Please run backup repair tool to restore backup "
+ + "system integrity");
+ throw e;
+ }
+ }
+
+ private String[] convertToBackupIds(List history) {
+ String[] ids = new String[history.size()];
+ for (int i = 0; i < ids.length; i++) {
+ ids[i] = history.get(i).getBackupId();
+ }
+ return ids;
+ }
+
+ private void executeDeleteListOfBackups(CommandLine cmdline) throws IOException {
+ String value = cmdline.getOptionValue(OPTION_LIST);
+ String[] backupIds = value.split(",");
+
+ try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ int deleted = admin.deleteBackups(backupIds);
+ System.out.println("Deleted " + deleted + " backups. Total requested: " + backupIds.length);
+ } catch (IOException e) {
+ System.err.println("Delete command FAILED. Please run backup repair tool to restore backup "
+ + "system integrity");
+ throw e;
+ }
+
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(DELETE_CMD_USAGE);
+ Options options = new Options();
+ options.addOption(OPTION_KEEP, true, OPTION_KEEP_DESC);
+ options.addOption(OPTION_LIST, true, OPTION_BACKUP_LIST_DESC);
+
+ HelpFormatter helpFormatter = new HelpFormatter();
+ helpFormatter.setLeftPadding(2);
+ helpFormatter.setDescPadding(8);
+ helpFormatter.setWidth(100);
+ helpFormatter.setSyntaxPrefix("Options:");
+ helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
+
+ }
+ }
+
+ public static class RepairCommand extends Command {
+ RepairCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ super.execute();
+
+ String[] args = cmdline == null ? null : cmdline.getArgs();
+ if (args != null && args.length > 1) {
+ System.err.println("ERROR: wrong number of arguments: " + args.length);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ Configuration conf = getConf() != null ? getConf() : HBaseConfiguration.create();
+ try (final Connection conn = ConnectionFactory.createConnection(conf);
+ final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
+ // Failed backup
+ BackupInfo backupInfo;
+ List list = sysTable.getBackupInfos(BackupState.RUNNING);
+ if (list.size() == 0) {
+ // No failed sessions found
+ System.out.println("REPAIR status: no failed sessions found."
+ + " Checking failed delete backup operation ...");
+ repairFailedBackupDeletionIfAny(conn, sysTable);
+ repairFailedBackupMergeIfAny(conn, sysTable);
+ return;
+ }
+ backupInfo = list.get(0);
+ // If this is a cancel exception, then we've already cleaned.
+ // set the failure timestamp of the overall backup
+ backupInfo.setCompleteTs(EnvironmentEdgeManager.currentTime());
+ // set failure message
+ backupInfo.setFailedMsg("REPAIR status: repaired after failure:\n" + backupInfo);
+ // set overall backup status: failed
+ backupInfo.setState(BackupState.FAILED);
+ // compose the backup failed data
+ String backupFailedData = "BackupId=" + backupInfo.getBackupId() + ",startts="
+ + backupInfo.getStartTs() + ",failedts=" + backupInfo.getCompleteTs() + ",failedphase="
+ + backupInfo.getPhase() + ",failedmessage=" + backupInfo.getFailedMsg();
+ System.out.println(backupFailedData);
+ TableBackupClient.cleanupAndRestoreBackupSystem(conn, backupInfo, conf);
+ // If backup session is updated to FAILED state - means we
+ // processed recovery already.
+ sysTable.updateBackupInfo(backupInfo);
+ sysTable.finishBackupExclusiveOperation();
+ System.out.println("REPAIR status: finished repair failed session:\n " + backupInfo);
+ }
+ }
+
+ private void repairFailedBackupDeletionIfAny(Connection conn, BackupSystemTable sysTable)
+ throws IOException {
+ String[] backupIds = sysTable.getListOfBackupIdsFromDeleteOperation();
+ if (backupIds == null || backupIds.length == 0) {
+ System.out.println("No failed backup DELETE operation found");
+ // Delete backup table snapshot if exists
+ BackupSystemTable.deleteSnapshot(conn);
+ return;
+ }
+ System.out.println("Found failed DELETE operation for: " + StringUtils.join(backupIds));
+ System.out.println("Running DELETE again ...");
+ // Restore table from snapshot
+ BackupSystemTable.restoreFromSnapshot(conn);
+ // Finish previous failed session
+ sysTable.finishBackupExclusiveOperation();
+ try (BackupAdmin admin = new BackupAdminImpl(conn)) {
+ admin.deleteBackups(backupIds);
+ }
+ System.out.println("DELETE operation finished OK: " + StringUtils.join(backupIds));
+ }
+
+ public static void repairFailedBackupMergeIfAny(Connection conn, BackupSystemTable sysTable)
+ throws IOException {
+
+ String[] backupIds = sysTable.getListOfBackupIdsFromMergeOperation();
+ if (backupIds == null || backupIds.length == 0) {
+ System.out.println("No failed backup MERGE operation found");
+ // Delete backup table snapshot if exists
+ BackupSystemTable.deleteSnapshot(conn);
+ return;
+ }
+ System.out.println("Found failed MERGE operation for: " + StringUtils.join(backupIds));
+ // Check if backup .tmp exists
+ BackupInfo bInfo = sysTable.readBackupInfo(backupIds[0]);
+ String backupRoot = bInfo.getBackupRootDir();
+ FileSystem fs = FileSystem.get(new Path(backupRoot).toUri(), new Configuration());
+ String backupId = BackupUtils.findMostRecentBackupId(backupIds);
+ Path tmpPath = HBackupFileSystem.getBackupTmpDirPathForBackupId(backupRoot, backupId);
+ if (fs.exists(tmpPath)) {
+ // Move data back
+ Path destPath = HBackupFileSystem.getBackupPath(backupRoot, backupId);
+ if (!fs.delete(destPath, true)) {
+ System.out.println("Failed to delete " + destPath);
+ }
+ boolean res = fs.rename(tmpPath, destPath);
+ if (!res) {
+ throw new IOException(
+ "MERGE repair: failed to rename from " + tmpPath + " to " + destPath);
+ }
+ System.out
+ .println("MERGE repair: renamed from " + tmpPath + " to " + destPath + " res=" + res);
+ } else {
+ checkRemoveBackupImages(fs, backupRoot, backupIds);
+ }
+ // Restore table from snapshot
+ BackupSystemTable.restoreFromSnapshot(conn);
+ // Unlock backup system
+ sysTable.finishBackupExclusiveOperation();
+ // Finish previous failed session
+ sysTable.finishMergeOperation();
+
+ System.out.println("MERGE repair operation finished OK: " + StringUtils.join(backupIds));
+ }
+
+ private static void checkRemoveBackupImages(FileSystem fs, String backupRoot,
+ String[] backupIds) throws IOException {
+ String mergedBackupId = BackupUtils.findMostRecentBackupId(backupIds);
+ for (String backupId : backupIds) {
+ if (backupId.equals(mergedBackupId)) {
+ continue;
+ }
+ Path path = HBackupFileSystem.getBackupPath(backupRoot, backupId);
+ if (fs.exists(path)) {
+ if (!fs.delete(path, true)) {
+ System.out.println("MERGE repair removing: " + path + " - FAILED");
+ } else {
+ System.out.println("MERGE repair removing: " + path + " - OK");
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(REPAIR_CMD_USAGE);
+ }
+ }
+
+ public static class MergeCommand extends Command {
+ MergeCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ protected boolean requiresNoActiveSession() {
+ return true;
+ }
+
+ @Override
+ protected boolean requiresConsistentState() {
+ return true;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ super.execute();
+
+ String[] args = cmdline == null ? null : cmdline.getArgs();
+ if (args == null || (args.length != 2)) {
+ System.err
+ .println("ERROR: wrong number of arguments: " + (args == null ? null : args.length));
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ String[] backupIds = args[1].split(",");
+ if (backupIds.length < 2) {
+ String msg = "ERROR: can not merge a single backup image. "
+ + "Number of images must be greater than 1.";
+ System.err.println(msg);
+ throw new IOException(msg);
+
+ }
+ Configuration conf = getConf() != null ? getConf() : HBaseConfiguration.create();
+ try (final Connection conn = ConnectionFactory.createConnection(conf);
+ final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ admin.mergeBackups(backupIds);
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(MERGE_CMD_USAGE);
+ }
+ }
+
+ public static class HistoryCommand extends Command {
+ private final static int DEFAULT_HISTORY_LENGTH = 10;
+
+ HistoryCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ int n = parseHistoryLength();
+ final TableName tableName = getTableName();
+ final String setName = getTableSetName();
+ BackupInfo.Filter tableNameFilter = new BackupInfo.Filter() {
+ @Override
+ public boolean apply(BackupInfo info) {
+ if (tableName == null) {
+ return true;
+ }
+
+ List names = info.getTableNames();
+ return names.contains(tableName);
+ }
+ };
+ BackupInfo.Filter tableSetFilter = new BackupInfo.Filter() {
+ @Override
+ public boolean apply(BackupInfo info) {
+ if (setName == null) {
+ return true;
+ }
+
+ String backupId = info.getBackupId();
+ return backupId.startsWith(setName);
+ }
+ };
+ Path backupRootPath = getBackupRootPath();
+ List history;
+ if (backupRootPath == null) {
+ // Load from backup system table
+ super.execute();
+ try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
+ history = sysTable.getBackupHistory(n, tableNameFilter, tableSetFilter);
+ }
+ } else {
+ // load from backup FS
+ history =
+ BackupUtils.getHistory(getConf(), n, backupRootPath, tableNameFilter, tableSetFilter);
+ }
+ for (BackupInfo info : history) {
+ System.out.println(info.getShortDescription());
+ }
+ }
+
+ private Path getBackupRootPath() throws IOException {
+ String value = null;
+ try {
+ value = cmdline.getOptionValue(OPTION_PATH);
+
+ if (value == null) {
+ return null;
+ }
+
+ return new Path(value);
+ } catch (IllegalArgumentException e) {
+ System.out.println("ERROR: Illegal argument for backup root path: " + value);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ }
+
+ private TableName getTableName() throws IOException {
+ String value = cmdline.getOptionValue(OPTION_TABLE);
+
+ if (value == null) {
+ return null;
+ }
+
+ try {
+ return TableName.valueOf(value);
+ } catch (IllegalArgumentException e) {
+ System.out.println("Illegal argument for table name: " + value);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ }
+
+ private String getTableSetName() {
+ return cmdline.getOptionValue(OPTION_SET);
+ }
+
+ private int parseHistoryLength() throws IOException {
+ String value = cmdline.getOptionValue(OPTION_RECORD_NUMBER);
+ try {
+ if (value == null) {
+ return DEFAULT_HISTORY_LENGTH;
+ }
+
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ System.out.println("Illegal argument for history length: " + value);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(HISTORY_CMD_USAGE);
+ Options options = new Options();
+ options.addOption(OPTION_RECORD_NUMBER, true, OPTION_RECORD_NUMBER_DESC);
+ options.addOption(OPTION_PATH, true, OPTION_PATH_DESC);
+ options.addOption(OPTION_TABLE, true, OPTION_TABLE_DESC);
+ options.addOption(OPTION_SET, true, OPTION_SET_DESC);
+
+ HelpFormatter helpFormatter = new HelpFormatter();
+ helpFormatter.setLeftPadding(2);
+ helpFormatter.setDescPadding(8);
+ helpFormatter.setWidth(100);
+ helpFormatter.setSyntaxPrefix("Options:");
+ helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
+ }
+ }
+
+ public static class BackupSetCommand extends Command {
+ private final static String SET_ADD_CMD = "add";
+ private final static String SET_REMOVE_CMD = "remove";
+ private final static String SET_DELETE_CMD = "delete";
+ private final static String SET_DESCRIBE_CMD = "describe";
+ private final static String SET_LIST_CMD = "list";
+
+ BackupSetCommand(Configuration conf, CommandLine cmdline) {
+ super(conf);
+ this.cmdline = cmdline;
+ }
+
+ @Override
+ public void execute() throws IOException {
+ // Command-line must have at least one element
+ if (cmdline == null || cmdline.getArgs() == null || cmdline.getArgs().length < 2) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+
+ String[] args = cmdline.getArgs();
+ String cmdStr = args[1];
+ BackupCommand cmd = getCommand(cmdStr);
+
+ switch (cmd) {
+ case SET_ADD:
+ processSetAdd(args);
+ break;
+ case SET_REMOVE:
+ processSetRemove(args);
+ break;
+ case SET_DELETE:
+ processSetDelete(args);
+ break;
+ case SET_DESCRIBE:
+ processSetDescribe(args);
+ break;
+ case SET_LIST:
+ processSetList();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void processSetList() throws IOException {
+ super.execute();
+
+ // List all backup set names
+ // does not expect any args
+ try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ List list = admin.listBackupSets();
+ for (BackupSet bs : list) {
+ System.out.println(bs);
+ }
+ }
+ }
+
+ private void processSetDescribe(String[] args) throws IOException {
+ if (args == null || args.length != 3) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ super.execute();
+
+ String setName = args[2];
+ try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
+ List tables = sysTable.describeBackupSet(setName);
+ BackupSet set = tables == null ? null : new BackupSet(setName, tables);
+ if (set == null) {
+ System.out.println("Set '" + setName + "' does not exist.");
+ } else {
+ System.out.println(set);
+ }
+ }
+ }
+
+ private void processSetDelete(String[] args) throws IOException {
+ if (args == null || args.length != 3) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ super.execute();
+
+ String setName = args[2];
+ try (final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ boolean result = admin.deleteBackupSet(setName);
+ if (result) {
+ System.out.println("Delete set " + setName + " OK.");
+ } else {
+ System.out.println("Set " + setName + " does not exist");
+ }
+ }
+ }
+
+ private void processSetRemove(String[] args) throws IOException {
+ if (args == null || args.length != 4) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ super.execute();
+
+ String setName = args[2];
+ String[] tables = args[3].split(",");
+ TableName[] tableNames = toTableNames(tables);
+ try (final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ admin.removeFromBackupSet(setName, tableNames);
+ }
+ }
+
+ private TableName[] toTableNames(String[] tables) {
+ TableName[] arr = new TableName[tables.length];
+ for (int i = 0; i < tables.length; i++) {
+ arr[i] = TableName.valueOf(tables[i]);
+ }
+ return arr;
+ }
+
+ private void processSetAdd(String[] args) throws IOException {
+ if (args == null || args.length != 4) {
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ super.execute();
+ String setName = args[2];
+ TableName[] tableNames =
+ Splitter.on(',').splitToStream(args[3]).map(TableName::valueOf).toArray(TableName[]::new);
+ try (final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
+ admin.addToBackupSet(setName, tableNames);
+ }
+ }
+
+ private BackupCommand getCommand(String cmdStr) throws IOException {
+ switch (cmdStr) {
+ case SET_ADD_CMD:
+ return BackupCommand.SET_ADD;
+ case SET_REMOVE_CMD:
+ return BackupCommand.SET_REMOVE;
+ case SET_DELETE_CMD:
+ return BackupCommand.SET_DELETE;
+ case SET_DESCRIBE_CMD:
+ return BackupCommand.SET_DESCRIBE;
+ case SET_LIST_CMD:
+ return BackupCommand.SET_LIST;
+ default:
+ System.out.println("ERROR: Unknown command for 'set' :" + cmdStr);
+ printUsage();
+ throw new IOException(INCORRECT_USAGE);
+ }
+ }
+
+ @Override
+ protected void printUsage() {
+ System.out.println(SET_CMD_USAGE);
+ }
+ }
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupException.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupException.java
new file mode 100644
index 000000000000..8dd262cbb88f
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupException.java
@@ -0,0 +1,82 @@
+/*
+ * 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 org.apache.hadoop.hbase.HBaseIOException;
+import org.apache.hadoop.hbase.backup.BackupInfo;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Backup exception
+ */
+@SuppressWarnings("serial")
+@InterfaceAudience.Private
+public class BackupException extends HBaseIOException {
+ private BackupInfo info;
+
+ /**
+ * Some exception happened for a backup and don't even know the backup that it was about
+ * @param msg Full description of the failure
+ */
+ public BackupException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Some exception happened for a backup with a cause
+ * @param cause the cause
+ */
+ public BackupException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Exception for the given backup that has no previous root cause
+ * @param msg reason why the backup failed
+ * @param desc description of the backup that is being failed
+ */
+ public BackupException(String msg, BackupInfo desc) {
+ super(msg);
+ this.info = desc;
+ }
+
+ /**
+ * Exception for the given backup due to another exception
+ * @param msg reason why the backup failed
+ * @param cause root cause of the failure
+ * @param desc description of the backup that is being failed
+ */
+ public BackupException(String msg, Throwable cause, BackupInfo desc) {
+ super(msg, cause);
+ this.info = desc;
+ }
+
+ /**
+ * Exception when the description of the backup cannot be determined, due to some other root cause
+ * @param message description of what caused the failure
+ * @param e root cause
+ */
+ public BackupException(String message, Exception e) {
+ super(message, e);
+ }
+
+ public BackupInfo getBackupInfo() {
+ return this.info;
+ }
+
+}
diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupManager.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupManager.java
new file mode 100644
index 000000000000..a543b577b7ae
--- /dev/null
+++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/BackupManager.java
@@ -0,0 +1,521 @@
+/*
+ * 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.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.backup.BackupHFileCleaner;
+import org.apache.hadoop.hbase.backup.BackupInfo;
+import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
+import org.apache.hadoop.hbase.backup.BackupObserver;
+import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
+import org.apache.hadoop.hbase.backup.BackupType;
+import org.apache.hadoop.hbase.backup.HBackupFileSystem;
+import org.apache.hadoop.hbase.backup.impl.BackupManifest.BackupImage;
+import org.apache.hadoop.hbase.backup.master.BackupLogCleaner;
+import org.apache.hadoop.hbase.backup.master.LogRollMasterProcedureManager;
+import org.apache.hadoop.hbase.backup.regionserver.LogRollRegionServerProcedureManager;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
+import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
+import org.apache.hadoop.hbase.procedure.ProcedureManagerHost;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.apache.hadoop.hbase.util.Pair;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles backup requests, creates backup info records in backup system table to keep track of
+ * backup sessions, dispatches backup request.
+ */
+@InterfaceAudience.Private
+public class BackupManager implements Closeable {
+ // in seconds
+ public final static String BACKUP_EXCLUSIVE_OPERATION_TIMEOUT_SECONDS_KEY =
+ "hbase.backup.exclusive.op.timeout.seconds";
+ // In seconds
+ private final static int DEFAULT_BACKUP_EXCLUSIVE_OPERATION_TIMEOUT = 3600;
+ private static final Logger LOG = LoggerFactory.getLogger(BackupManager.class);
+
+ protected Configuration conf = null;
+ protected BackupInfo backupInfo = null;
+ protected BackupSystemTable systemTable;
+ protected final Connection conn;
+
+ /**
+ * Backup manager constructor.
+ * @param conn connection
+ * @param conf configuration
+ * @throws IOException exception
+ */
+ public BackupManager(Connection conn, Configuration conf) throws IOException {
+ if (
+ !conf.getBoolean(BackupRestoreConstants.BACKUP_ENABLE_KEY,
+ BackupRestoreConstants.BACKUP_ENABLE_DEFAULT)
+ ) {
+ throw new BackupException("HBase backup is not enabled. Check your "
+ + BackupRestoreConstants.BACKUP_ENABLE_KEY + " setting.");
+ }
+ this.conf = conf;
+ this.conn = conn;
+ this.systemTable = new BackupSystemTable(conn);
+ }
+
+ /**
+ * Returns backup info
+ */
+ protected BackupInfo getBackupInfo() {
+ return backupInfo;
+ }
+
+ /**
+ * This method modifies the master's configuration in order to inject backup-related features
+ * (TESTs only)
+ * @param conf configuration
+ */
+ public static void decorateMasterConfiguration(Configuration conf) {
+ if (!isBackupEnabled(conf)) {
+ return;
+ }
+ // Add WAL archive cleaner plug-in
+ String plugins = conf.get(HConstants.HBASE_MASTER_LOGCLEANER_PLUGINS);
+ String cleanerClass = BackupLogCleaner.class.getCanonicalName();
+ if (!plugins.contains(cleanerClass)) {
+ conf.set(HConstants.HBASE_MASTER_LOGCLEANER_PLUGINS, plugins + "," + cleanerClass);
+ }
+
+ String classes = conf.get(ProcedureManagerHost.MASTER_PROCEDURE_CONF_KEY);
+ String masterProcedureClass = LogRollMasterProcedureManager.class.getName();
+ if (classes == null) {
+ conf.set(ProcedureManagerHost.MASTER_PROCEDURE_CONF_KEY, masterProcedureClass);
+ } else if (!classes.contains(masterProcedureClass)) {
+ conf.set(ProcedureManagerHost.MASTER_PROCEDURE_CONF_KEY,
+ classes + "," + masterProcedureClass);
+ }
+
+ plugins = conf.get(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS);
+ conf.set(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
+ (plugins == null ? "" : plugins + ",") + BackupHFileCleaner.class.getName());
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "Added log cleaner: {}. Added master procedure manager: {}."
+ + "Added master procedure manager: {}",
+ cleanerClass, masterProcedureClass, BackupHFileCleaner.class.getName());
+ }
+ }
+
+ /**
+ * This method modifies the Region Server configuration in order to inject backup-related features
+ * TESTs only.
+ * @param conf configuration
+ */
+ public static void decorateRegionServerConfiguration(Configuration conf) {
+ if (!isBackupEnabled(conf)) {
+ return;
+ }
+
+ String classes = conf.get(ProcedureManagerHost.REGIONSERVER_PROCEDURE_CONF_KEY);
+ String regionProcedureClass = LogRollRegionServerProcedureManager.class.getName();
+ if (classes == null) {
+ conf.set(ProcedureManagerHost.REGIONSERVER_PROCEDURE_CONF_KEY, regionProcedureClass);
+ } else if (!classes.contains(regionProcedureClass)) {
+ conf.set(ProcedureManagerHost.REGIONSERVER_PROCEDURE_CONF_KEY,
+ classes + "," + regionProcedureClass);
+ }
+ String coproc = conf.get(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
+ String regionObserverClass = BackupObserver.class.getName();
+ conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
+ (coproc == null ? "" : coproc + ",") + regionObserverClass);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Added region procedure manager: {}. Added region observer: {}",
+ regionProcedureClass, regionObserverClass);
+ }
+ }
+
+ public static boolean isBackupEnabled(Configuration conf) {
+ return conf.getBoolean(BackupRestoreConstants.BACKUP_ENABLE_KEY,
+ BackupRestoreConstants.BACKUP_ENABLE_DEFAULT);
+ }
+
+ /**
+ * Get configuration n
+ */
+ Configuration getConf() {
+ return conf;
+ }
+
+ /**
+ * Stop all the work of backup.
+ */
+ @Override
+ public void close() {
+ if (systemTable != null) {
+ try {
+ systemTable.close();
+ } catch (Exception e) {
+ LOG.error(e.toString(), e);
+ }
+ }
+ }
+
+ /**
+ * Creates a backup info based on input backup request.
+ * @param backupId backup id
+ * @param type type
+ * @param tableList table list
+ * @param targetRootDir root dir
+ * @param workers number of parallel workers
+ * @param bandwidth bandwidth per worker in MB per sec n * @throws BackupException exception
+ */
+ public BackupInfo createBackupInfo(String backupId, BackupType type, List tableList,
+ String targetRootDir, int workers, long bandwidth) throws BackupException {
+ if (targetRootDir == null) {
+ throw new BackupException("Wrong backup request parameter: target backup root directory");
+ }
+
+ if (type == BackupType.FULL && (tableList == null || tableList.isEmpty())) {
+ // If table list is null for full backup, which means backup all tables. Then fill the table
+ // list with all user tables from meta. It no table available, throw the request exception.
+ List htds = null;
+ try (Admin admin = conn.getAdmin()) {
+ htds = admin.listTableDescriptors();
+ } catch (Exception e) {
+ throw new BackupException(e);
+ }
+
+ if (htds == null) {
+ throw new BackupException("No table exists for full backup of all tables.");
+ } else {
+ tableList = new ArrayList<>();
+ for (TableDescriptor hTableDescriptor : htds) {
+ TableName tn = hTableDescriptor.getTableName();
+ if (tn.equals(BackupSystemTable.getTableName(conf))) {
+ // skip backup system table
+ continue;
+ }
+ tableList.add(hTableDescriptor.getTableName());
+ }
+
+ LOG.info("Full backup all the tables available in the cluster: {}", tableList);
+ }
+ }
+
+ // there are one or more tables in the table list
+ backupInfo = new BackupInfo(backupId, type, tableList.toArray(new TableName[tableList.size()]),
+ targetRootDir);
+ backupInfo.setBandwidth(bandwidth);
+ backupInfo.setWorkers(workers);
+ return backupInfo;
+ }
+
+ /**
+ * Check if any ongoing backup. Currently, we only reply on checking status in backup system
+ * table. We need to consider to handle the case of orphan records in the future. Otherwise, all
+ * the coming request will fail.
+ * @return the ongoing backup id if on going backup exists, otherwise null
+ * @throws IOException exception
+ */
+ private String getOngoingBackupId() throws IOException {
+ ArrayList sessions = systemTable.getBackupInfos(BackupState.RUNNING);
+ if (sessions.size() == 0) {
+ return null;
+ }
+ return sessions.get(0).getBackupId();
+ }
+
+ /**
+ * Start the backup manager service.
+ * @throws IOException exception
+ */
+ public void initialize() throws IOException {
+ String ongoingBackupId = this.getOngoingBackupId();
+ if (ongoingBackupId != null) {
+ LOG.info("There is a ongoing backup {}"
+ + ". Can not launch new backup until no ongoing backup remains.", ongoingBackupId);
+ throw new BackupException("There is ongoing backup seesion.");
+ }
+ }
+
+ public void setBackupInfo(BackupInfo backupInfo) {
+ this.backupInfo = backupInfo;
+ }
+
+ /**
+ * Get direct ancestors of the current backup.
+ * @param backupInfo The backup info for the current backup
+ * @return The ancestors for the current backup
+ * @throws IOException exception
+ */
+ public ArrayList getAncestors(BackupInfo backupInfo) throws IOException {
+ LOG.debug("Getting the direct ancestors of the current backup {}", backupInfo.getBackupId());
+
+ ArrayList ancestors = new ArrayList<>();
+
+ // full backup does not have ancestor
+ if (backupInfo.getType() == BackupType.FULL) {
+ LOG.debug("Current backup is a full backup, no direct ancestor for it.");
+ return ancestors;
+ }
+
+ // get all backup history list in descending order
+ ArrayList allHistoryList = getBackupHistory(true);
+ for (BackupInfo backup : allHistoryList) {
+
+ BackupImage.Builder builder = BackupImage.newBuilder();
+
+ BackupImage image = builder.withBackupId(backup.getBackupId()).withType(backup.getType())
+ .withRootDir(backup.getBackupRootDir()).withTableList(backup.getTableNames())
+ .withStartTime(backup.getStartTs()).withCompleteTime(backup.getCompleteTs()).build();
+
+ // Only direct ancestors for a backup are required and not entire history of backup for this
+ // table resulting in verifying all of the previous backups which is unnecessary and backup
+ // paths need not be valid beyond the lifetime of a backup.
+ //
+ // RootDir is way of grouping a single backup including one full and many incremental backups
+ if (!image.getRootDir().equals(backupInfo.getBackupRootDir())) {
+ continue;
+ }
+
+ // add the full backup image as an ancestor until the last incremental backup
+ if (backup.getType().equals(BackupType.FULL)) {
+ // check the backup image coverage, if previous image could be covered by the newer ones,
+ // then no need to add
+ if (!BackupManifest.canCoverImage(ancestors, image)) {
+ ancestors.add(image);
+ }
+ } else {
+ // found last incremental backup, if previously added full backup ancestor images can cover
+ // it, then this incremental ancestor is not the dependent of the current incremental
+ // backup, that is to say, this is the backup scope boundary of current table set.
+ // Otherwise, this incremental backup ancestor is the dependent ancestor of the ongoing
+ // incremental backup
+ if (BackupManifest.canCoverImage(ancestors, image)) {
+ LOG.debug("Met the backup boundary of the current table set:");
+ for (BackupImage image1 : ancestors) {
+ LOG.debug(" BackupID={}, BackupDir={}", image1.getBackupId(), image1.getRootDir());
+ }
+ } else {
+ Path logBackupPath =
+ HBackupFileSystem.getBackupPath(backup.getBackupRootDir(), backup.getBackupId());
+ LOG.debug(
+ "Current backup has an incremental backup ancestor, "
+ + "touching its image manifest in {}" + " to construct the dependency.",
+ logBackupPath.toString());
+ BackupManifest lastIncrImgManifest = new BackupManifest(conf, logBackupPath);
+ BackupImage lastIncrImage = lastIncrImgManifest.getBackupImage();
+ ancestors.add(lastIncrImage);
+
+ LOG.debug("Last dependent incremental backup image: {BackupID={}" + "BackupDir={}}",
+ lastIncrImage.getBackupId(), lastIncrImage.getRootDir());
+ }
+ }
+ }
+ LOG.debug("Got {} ancestors for the current backup.", ancestors.size());
+ return ancestors;
+ }
+
+ /**
+ * Get the direct ancestors of this backup for one table involved.
+ * @param backupInfo backup info
+ * @param table table
+ * @return backupImages on the dependency list
+ * @throws IOException exception
+ */
+ public ArrayList getAncestors(BackupInfo backupInfo, TableName table)
+ throws IOException {
+ ArrayList ancestors = getAncestors(backupInfo);
+ ArrayList tableAncestors = new ArrayList<>();
+ for (BackupImage image : ancestors) {
+ if (image.hasTable(table)) {
+ tableAncestors.add(image);
+ if (image.getType() == BackupType.FULL) {
+ break;
+ }
+ }
+ }
+ return tableAncestors;
+ }
+
+ /*
+ * backup system table operations
+ */
+
+ /**
+ * Updates status (state) of a backup session in a persistent store
+ * @param context context
+ * @throws IOException exception
+ */
+ public void updateBackupInfo(BackupInfo context) throws IOException {
+ systemTable.updateBackupInfo(context);
+ }
+
+ /**
+ * Starts new backup session
+ * @throws IOException if active session already exists
+ */
+ public void startBackupSession() throws IOException {
+ long startTime = EnvironmentEdgeManager.currentTime();
+ long timeout = conf.getInt(BACKUP_EXCLUSIVE_OPERATION_TIMEOUT_SECONDS_KEY,
+ DEFAULT_BACKUP_EXCLUSIVE_OPERATION_TIMEOUT) * 1000L;
+ long lastWarningOutputTime = 0;
+ while (EnvironmentEdgeManager.currentTime() - startTime < timeout) {
+ try {
+ systemTable.startBackupExclusiveOperation();
+ return;
+ } catch (IOException e) {
+ if (e instanceof ExclusiveOperationException) {
+ // sleep, then repeat
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e1) {
+ // Restore the interrupted status
+ Thread.currentThread().interrupt();
+ }
+ if (
+ lastWarningOutputTime == 0
+ || (EnvironmentEdgeManager.currentTime() - lastWarningOutputTime) > 60000
+ ) {
+ lastWarningOutputTime = EnvironmentEdgeManager.currentTime();
+ LOG.warn("Waiting to acquire backup exclusive lock for {}s",
+ +(lastWarningOutputTime - startTime) / 1000);
+ }
+ } else {
+ throw e;
+ }
+ }
+ }
+ throw new IOException(
+ "Failed to acquire backup system table exclusive lock after " + timeout / 1000 + "s");
+ }
+
+ /**
+ * Finishes active backup session
+ * @throws IOException if no active session
+ */
+ public void finishBackupSession() throws IOException {
+ systemTable.finishBackupExclusiveOperation();
+ }
+
+ /**
+ * Read the last backup start code (timestamp) of last successful backup. Will return null if
+ * there is no startcode stored in backup system table or the value is of length 0. These two
+ * cases indicate there is no successful backup completed so far.
+ * @return the timestamp of a last successful backup
+ * @throws IOException exception
+ */
+ public String readBackupStartCode() throws IOException {
+ return systemTable.readBackupStartCode(backupInfo.getBackupRootDir());
+ }
+
+ /**
+ * Write the start code (timestamp) to backup system table. If passed in null, then write 0 byte.
+ * @param startCode start code
+ * @throws IOException exception
+ */
+ public void writeBackupStartCode(Long startCode) throws IOException {
+ systemTable.writeBackupStartCode(startCode, backupInfo.getBackupRootDir());
+ }
+
+ /**
+ * Get the RS log information after the last log roll from backup system table.
+ * @return RS log info
+ * @throws IOException exception
+ */
+ public HashMap readRegionServerLastLogRollResult() throws IOException {
+ return systemTable.readRegionServerLastLogRollResult(backupInfo.getBackupRootDir());
+ }
+
+ public Pair
+ */
+@InterfaceAudience.Private
+public final class BackupSystemTable implements Closeable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(BackupSystemTable.class);
+
+ static class WALItem {
+ String backupId;
+ String walFile;
+ String backupRoot;
+
+ WALItem(String backupId, String walFile, String backupRoot) {
+ this.backupId = backupId;
+ this.walFile = walFile;
+ this.backupRoot = backupRoot;
+ }
+
+ public String getBackupId() {
+ return backupId;
+ }
+
+ public String getWalFile() {
+ return walFile;
+ }
+
+ public String getBackupRoot() {
+ return backupRoot;
+ }
+
+ @Override
+ public String toString() {
+ return Path.SEPARATOR + backupRoot + Path.SEPARATOR + backupId + Path.SEPARATOR + walFile;
+ }
+ }
+
+ /**
+ * Backup system table (main) name
+ */
+ private TableName tableName;
+
+ /**
+ * Backup System table name for bulk loaded files. We keep all bulk loaded file references in a
+ * separate table because we have to isolate general backup operations: create, merge etc from
+ * activity of RegionObserver, which controls process of a bulk loading
+ * {@link org.apache.hadoop.hbase.backup.BackupObserver}
+ */
+ private TableName bulkLoadTableName;
+
+ /**
+ * Stores backup sessions (contexts)
+ */
+ final static byte[] SESSIONS_FAMILY = Bytes.toBytes("session");
+ /**
+ * Stores other meta
+ */
+ final static byte[] META_FAMILY = Bytes.toBytes("meta");
+ final static byte[] BULK_LOAD_FAMILY = Bytes.toBytes("bulk");
+ /**
+ * Connection to HBase cluster, shared among all instances
+ */
+ private final Connection connection;
+
+ private final static String BACKUP_INFO_PREFIX = "session:";
+ private final static String START_CODE_ROW = "startcode:";
+ private final static byte[] ACTIVE_SESSION_ROW = Bytes.toBytes("activesession:");
+ private final static byte[] ACTIVE_SESSION_COL = Bytes.toBytes("c");
+
+ private final static byte[] ACTIVE_SESSION_YES = Bytes.toBytes("yes");
+ private final static byte[] ACTIVE_SESSION_NO = Bytes.toBytes("no");
+
+ private final static String INCR_BACKUP_SET = "incrbackupset:";
+ private final static String TABLE_RS_LOG_MAP_PREFIX = "trslm:";
+ private final static String RS_LOG_TS_PREFIX = "rslogts:";
+
+ private final static String BULK_LOAD_PREFIX = "bulk:";
+ private final static byte[] BULK_LOAD_PREFIX_BYTES = Bytes.toBytes(BULK_LOAD_PREFIX);
+ private final static byte[] DELETE_OP_ROW = Bytes.toBytes("delete_op_row");
+ private final static byte[] MERGE_OP_ROW = Bytes.toBytes("merge_op_row");
+
+ final static byte[] TBL_COL = Bytes.toBytes("tbl");
+ final static byte[] FAM_COL = Bytes.toBytes("fam");
+ final static byte[] PATH_COL = Bytes.toBytes("path");
+ final static byte[] STATE_COL = Bytes.toBytes("state");
+ // the two states a bulk loaded file can be
+ final static byte[] BL_PREPARE = Bytes.toBytes("R");
+ final static byte[] BL_COMMIT = Bytes.toBytes("D");
+
+ private final static String SET_KEY_PREFIX = "backupset:";
+
+ // separator between BULK_LOAD_PREFIX and ordinals
+ private final static String BLK_LD_DELIM = ":";
+ private final static byte[] EMPTY_VALUE = new byte[] {};
+
+ // Safe delimiter in a string
+ private final static String NULL = "\u0000";
+
+ public BackupSystemTable(Connection conn) throws IOException {
+ this.connection = conn;
+ Configuration conf = this.connection.getConfiguration();
+ tableName = BackupSystemTable.getTableName(conf);
+ bulkLoadTableName = BackupSystemTable.getTableNameForBulkLoadedData(conf);
+ checkSystemTable();
+ }
+
+ private void checkSystemTable() throws IOException {
+ try (Admin admin = connection.getAdmin()) {
+ verifyNamespaceExists(admin);
+ Configuration conf = connection.getConfiguration();
+ if (!admin.tableExists(tableName)) {
+ TableDescriptor backupHTD = BackupSystemTable.getSystemTableDescriptor(conf);
+ admin.createTable(backupHTD);
+ }
+ if (!admin.tableExists(bulkLoadTableName)) {
+ TableDescriptor blHTD = BackupSystemTable.getSystemTableForBulkLoadedDataDescriptor(conf);
+ admin.createTable(blHTD);
+ }
+ waitForSystemTable(admin, tableName);
+ waitForSystemTable(admin, bulkLoadTableName);
+ }
+ }
+
+ private void verifyNamespaceExists(Admin admin) throws IOException {
+ String namespaceName = tableName.getNamespaceAsString();
+ NamespaceDescriptor ns = NamespaceDescriptor.create(namespaceName).build();
+ NamespaceDescriptor[] list = admin.listNamespaceDescriptors();
+ boolean exists = false;
+ for (NamespaceDescriptor nsd : list) {
+ if (nsd.getName().equals(ns.getName())) {
+ exists = true;
+ break;
+ }
+ }
+ if (!exists) {
+ admin.createNamespace(ns);
+ }
+ }
+
+ private void waitForSystemTable(Admin admin, TableName tableName) throws IOException {
+ // Return fast if the table is available and avoid a log message
+ if (admin.tableExists(tableName) && admin.isTableAvailable(tableName)) {
+ return;
+ }
+ long TIMEOUT = 60000;
+ long startTime = EnvironmentEdgeManager.currentTime();
+ LOG.debug("Backup table {} is not present and available, waiting for it to become so",
+ tableName);
+ while (!admin.tableExists(tableName) || !admin.isTableAvailable(tableName)) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw (IOException) new InterruptedIOException().initCause(e);
+ }
+ if (EnvironmentEdgeManager.currentTime() - startTime > TIMEOUT) {
+ throw new IOException(
+ "Failed to create backup system table " + tableName + " after " + TIMEOUT + "ms");
+ }
+ }
+ LOG.debug("Backup table {} exists and available", tableName);
+ }
+
+ @Override
+ public void close() {
+ // do nothing
+ }
+
+ /**
+ * Updates status (state) of a backup session in backup system table table
+ * @param info backup info
+ * @throws IOException exception
+ */
+ public void updateBackupInfo(BackupInfo info) throws IOException {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("update backup status in backup system table for: " + info.getBackupId()
+ + " set status=" + info.getState());
+ }
+ try (Table table = connection.getTable(tableName)) {
+ Put put = createPutForBackupInfo(info);
+ table.put(put);
+ }
+ }
+
+ /*
+ * @param backupId the backup Id
+ * @return Map of rows to path of bulk loaded hfile
+ */
+ Map readBulkLoadedFiles(String backupId) throws IOException {
+ Scan scan = BackupSystemTable.createScanForBulkLoadedFiles(backupId);
+ try (Table table = connection.getTable(bulkLoadTableName);
+ ResultScanner scanner = table.getScanner(scan)) {
+ Result res = null;
+ Map map = new TreeMap<>(Bytes.BYTES_COMPARATOR);
+ while ((res = scanner.next()) != null) {
+ res.advance();
+ byte[] row = CellUtil.cloneRow(res.listCells().get(0));
+ for (Cell cell : res.listCells()) {
+ if (
+ CellUtil.compareQualifiers(cell, BackupSystemTable.PATH_COL, 0,
+ BackupSystemTable.PATH_COL.length) == 0
+ ) {
+ map.put(row, Bytes.toString(CellUtil.cloneValue(cell)));
+ }
+ }
+ }
+ return map;
+ }
+ }
+
+ /*
+ * Used during restore
+ * @param backupId the backup Id
+ * @param sTableList List of tables
+ * @return array of Map of family to List of Paths
+ */
+ public Map>[] readBulkLoadedFiles(String backupId, List sTableList)
+ throws IOException {
+ Scan scan = BackupSystemTable.createScanForBulkLoadedFiles(backupId);
+ @SuppressWarnings("unchecked")
+ Map>[] mapForSrc = new Map[sTableList == null ? 1 : sTableList.size()];
+ try (Table table = connection.getTable(bulkLoadTableName);
+ ResultScanner scanner = table.getScanner(scan)) {
+ Result res = null;
+ while ((res = scanner.next()) != null) {
+ res.advance();
+ TableName tbl = null;
+ byte[] fam = null;
+ String path = null;
+ for (Cell cell : res.listCells()) {
+ if (
+ CellUtil.compareQualifiers(cell, BackupSystemTable.TBL_COL, 0,
+ BackupSystemTable.TBL_COL.length) == 0
+ ) {
+ tbl = TableName.valueOf(CellUtil.cloneValue(cell));
+ } else if (
+ CellUtil.compareQualifiers(cell, BackupSystemTable.FAM_COL, 0,
+ BackupSystemTable.FAM_COL.length) == 0
+ ) {
+ fam = CellUtil.cloneValue(cell);
+ } else if (
+ CellUtil.compareQualifiers(cell, BackupSystemTable.PATH_COL, 0,
+ BackupSystemTable.PATH_COL.length) == 0
+ ) {
+ path = Bytes.toString(CellUtil.cloneValue(cell));
+ }
+ }
+ int srcIdx = IncrementalTableBackupClient.getIndex(tbl, sTableList);
+ if (srcIdx == -1) {
+ // the table is not among the query
+ continue;
+ }
+ if (mapForSrc[srcIdx] == null) {
+ mapForSrc[srcIdx] = new TreeMap<>(Bytes.BYTES_COMPARATOR);
+ }
+ List files;
+ if (!mapForSrc[srcIdx].containsKey(fam)) {
+ files = new ArrayList();
+ mapForSrc[srcIdx].put(fam, files);
+ } else {
+ files = mapForSrc[srcIdx].get(fam);
+ }
+ files.add(new Path(path));
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("found bulk loaded file : " + tbl + " " + Bytes.toString(fam) + " " + path);
+ }
+ }
+
+ return mapForSrc;
+ }
+ }
+
+ /**
+ * Deletes backup status from backup system table table
+ * @param backupId backup id
+ * @throws IOException exception
+ */
+ public void deleteBackupInfo(String backupId) throws IOException {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("delete backup status in backup system table for " + backupId);
+ }
+ try (Table table = connection.getTable(tableName)) {
+ Delete del = createDeleteForBackupInfo(backupId);
+ table.delete(del);
+ }
+ }
+
+ /*
+ * For postBulkLoadHFile() hook.
+ * @param tabName table name
+ * @param region the region receiving hfile
+ * @param finalPaths family and associated hfiles
+ */
+ public void writePathsPostBulkLoad(TableName tabName, byte[] region,
+ Map> finalPaths) throws IOException {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("write bulk load descriptor to backup " + tabName + " with " + finalPaths.size()
+ + " entries");
+ }
+ try (Table table = connection.getTable(bulkLoadTableName)) {
+ List puts = BackupSystemTable.createPutForCommittedBulkload(tabName, region, finalPaths);
+ table.put(puts);
+ LOG.debug("written " + puts.size() + " rows for bulk load of " + tabName);
+ }
+ }
+
+ /*
+ * For preCommitStoreFile() hook
+ * @param tabName table name
+ * @param region the region receiving hfile
+ * @param family column family
+ * @param pairs list of paths for hfiles
+ */
+ public void writeFilesForBulkLoadPreCommit(TableName tabName, byte[] region, final byte[] family,
+ final List> pairs) throws IOException {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "write bulk load descriptor to backup " + tabName + " with " + pairs.size() + " entries");
+ }
+ try (Table table = connection.getTable(bulkLoadTableName)) {
+ List puts =
+ BackupSystemTable.createPutForPreparedBulkload(tabName, region, family, pairs);
+ table.put(puts);
+ LOG.debug("written " + puts.size() + " rows for bulk load of " + tabName);
+ }
+ }
+
+ /*
+ * Removes rows recording bulk loaded hfiles from backup table
+ * @param lst list of table names
+ * @param rows the rows to be deleted
+ */
+ public void deleteBulkLoadedRows(List rows) throws IOException {
+ try (Table table = connection.getTable(bulkLoadTableName)) {
+ List lstDels = new ArrayList<>();
+ for (byte[] row : rows) {
+ Delete del = new Delete(row);
+ lstDels.add(del);
+ LOG.debug("orig deleting the row: " + Bytes.toString(row));
+ }
+ table.delete(lstDels);
+ LOG.debug("deleted " + rows.size() + " original bulkload rows");
+ }
+ }
+
+ /*
+ * Reads the rows from backup table recording bulk loaded hfiles
+ * @param tableList list of table names
+ * @return The keys of the Map are table, region and column family. Value of the map reflects
+ * whether the hfile was recorded by preCommitStoreFile hook (true)
+ */
+ public Pair