org.apache.hadoop.hbase.backup.master.FullTableBackupProcedure.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hbase.backup.master.FullTableBackupProcedure.java

Source

/**
 * 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.master;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.NotServingRegionException;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.backup.util.BackupClientUtil;
import org.apache.hadoop.hbase.backup.BackupInfo;
import org.apache.hadoop.hbase.backup.BackupRestoreServerFactory;
import org.apache.hadoop.hbase.backup.BackupType;
import org.apache.hadoop.hbase.backup.HBackupFileSystem;
import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase;
import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
import org.apache.hadoop.hbase.backup.impl.BackupManifest.BackupImage;
import org.apache.hadoop.hbase.backup.BackupCopyService;
import org.apache.hadoop.hbase.backup.impl.BackupException;
import org.apache.hadoop.hbase.backup.impl.BackupManager;
import org.apache.hadoop.hbase.backup.impl.BackupManifest;
import org.apache.hadoop.hbase.backup.impl.BackupRestoreConstants;
import org.apache.hadoop.hbase.backup.util.BackupServerUtil;
import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil;
import org.apache.hadoop.hbase.master.procedure.TableProcedureInterface;
import org.apache.hadoop.hbase.procedure.MasterProcedureManager;
import org.apache.hadoop.hbase.procedure2.StateMachineProcedure;
import org.apache.hadoop.hbase.protobuf.generated.BackupProtos;
import org.apache.hadoop.hbase.protobuf.generated.BackupProtos.FullTableBackupState;
import org.apache.hadoop.hbase.protobuf.generated.BackupProtos.ServerTimestamp;
import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos;
import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos.SnapshotDescription;
import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.FSUtils;

@InterfaceAudience.Private
public class FullTableBackupProcedure extends StateMachineProcedure<MasterProcedureEnv, FullTableBackupState>
        implements TableProcedureInterface {
    private static final Log LOG = LogFactory.getLog(FullTableBackupProcedure.class);

    private static final String SNAPSHOT_BACKUP_MAX_ATTEMPTS_KEY = "hbase.backup.snapshot.attempts.max";
    private static final int DEFAULT_SNAPSHOT_BACKUP_MAX_ATTEMPTS = 10;

    private static final String SNAPSHOT_BACKUP_ATTEMPTS_DELAY_KEY = "hbase.backup.snapshot.attempts.delay";
    private static final int DEFAULT_SNAPSHOT_BACKUP_ATTEMPTS_DELAY = 10000;

    private final AtomicBoolean aborted = new AtomicBoolean(false);
    private Configuration conf;
    private String backupId;
    private List<TableName> tableList;
    private String targetRootDir;
    HashMap<String, Long> newTimestamps = null;

    private BackupManager backupManager;
    private BackupInfo backupInfo;

    public FullTableBackupProcedure() {
        // Required by the Procedure framework to create the procedure on replay
    }

    public FullTableBackupProcedure(final MasterProcedureEnv env, final String backupId, List<TableName> tableList,
            String targetRootDir, final int workers, final long bandwidth) throws IOException {
        backupManager = new BackupManager(env.getMasterConfiguration());
        this.backupId = backupId;
        this.tableList = tableList;
        this.targetRootDir = targetRootDir;
        backupInfo = backupManager.createBackupInfo(backupId, BackupType.FULL, tableList, targetRootDir, workers,
                bandwidth);
        if (tableList == null || tableList.isEmpty()) {
            this.tableList = new ArrayList<>(backupInfo.getTables());
        }
    }

    @Override
    public byte[] getResult() {
        return backupId.getBytes();
    }

    /**
     * Begin the overall backup.
     * @param backupInfo backup context
     * @throws IOException exception
     */
    static void beginBackup(BackupManager backupManager, BackupInfo backupInfo) throws IOException {
        backupManager.setBackupInfo(backupInfo);
        // set the start timestamp of the overall backup
        long startTs = EnvironmentEdgeManager.currentTime();
        backupInfo.setStartTs(startTs);
        // set overall backup status: ongoing
        backupInfo.setState(BackupState.RUNNING);
        LOG.info("Backup " + backupInfo.getBackupId() + " started at " + startTs + ".");

        backupManager.updateBackupInfo(backupInfo);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Backup session " + backupInfo.getBackupId() + " has been started.");
        }
    }

    private static String getMessage(Exception e) {
        String msg = e.getMessage();
        if (msg == null || msg.equals("")) {
            msg = e.getClass().getName();
        }
        return msg;
    }

    /**
     * Delete HBase snapshot for backup.
     * @param backupCtx backup context
     * @throws Exception exception
     */
    private static void deleteSnapshot(final MasterProcedureEnv env, BackupInfo backupCtx, Configuration conf)
            throws IOException {
        LOG.debug("Trying to delete snapshot for full backup.");
        for (String snapshotName : backupCtx.getSnapshotNames()) {
            if (snapshotName == null) {
                continue;
            }
            LOG.debug("Trying to delete snapshot: " + snapshotName);
            HBaseProtos.SnapshotDescription.Builder builder = HBaseProtos.SnapshotDescription.newBuilder();
            builder.setName(snapshotName);
            try {
                env.getMasterServices().getSnapshotManager().deleteSnapshot(builder.build());
            } catch (IOException ioe) {
                LOG.debug("when deleting snapshot " + snapshotName, ioe);
            }
            LOG.debug("Deleting the snapshot " + snapshotName + " for backup " + backupCtx.getBackupId()
                    + " succeeded.");
        }
    }

    /**
     * Clean up directories with prefix "exportSnapshot-", which are generated when exporting
     * snapshots.
     * @throws IOException exception
     */
    private static void cleanupExportSnapshotLog(Configuration conf) throws IOException {
        FileSystem fs = FSUtils.getCurrentFileSystem(conf);
        Path stagingDir = new Path(
                conf.get(BackupRestoreConstants.CONF_STAGING_ROOT, fs.getWorkingDirectory().toString()));
        FileStatus[] files = FSUtils.listStatus(fs, stagingDir);
        if (files == null) {
            return;
        }
        for (FileStatus file : files) {
            if (file.getPath().getName().startsWith("exportSnapshot-")) {
                LOG.debug("Delete log files of exporting snapshot: " + file.getPath().getName());
                if (FSUtils.delete(fs, file.getPath(), true) == false) {
                    LOG.warn("Can not delete " + file.getPath());
                }
            }
        }
    }

    /**
     * Clean up the uncompleted data at target directory if the ongoing backup has already entered the
     * copy phase.
     */
    static void cleanupTargetDir(BackupInfo backupInfo, Configuration conf) {
        try {
            // clean up the uncompleted data at target directory if the ongoing backup has already entered
            // the copy phase
            LOG.debug("Trying to cleanup up target dir. Current backup phase: " + backupInfo.getPhase());
            if (backupInfo.getPhase().equals(BackupPhase.SNAPSHOTCOPY)
                    || backupInfo.getPhase().equals(BackupPhase.INCREMENTAL_COPY)
                    || backupInfo.getPhase().equals(BackupPhase.STORE_MANIFEST)) {
                FileSystem outputFs = FileSystem.get(new Path(backupInfo.getTargetRootDir()).toUri(), conf);

                // now treat one backup as a transaction, clean up data that has been partially copied at
                // table level
                for (TableName table : backupInfo.getTables()) {
                    Path targetDirPath = new Path(HBackupFileSystem.getTableBackupDir(backupInfo.getTargetRootDir(),
                            backupInfo.getBackupId(), table));
                    if (outputFs.delete(targetDirPath, true)) {
                        LOG.info("Cleaning up uncompleted backup data at " + targetDirPath.toString() + " done.");
                    } else {
                        LOG.info("No data has been copied to " + targetDirPath.toString() + ".");
                    }

                    Path tableDir = targetDirPath.getParent();
                    FileStatus[] backups = FSUtils.listStatus(outputFs, tableDir);
                    if (backups == null || backups.length == 0) {
                        outputFs.delete(tableDir, true);
                        LOG.debug(tableDir.toString() + " is empty, remove it.");
                    }
                }
            }

        } catch (IOException e1) {
            LOG.error("Cleaning up uncompleted backup data of " + backupInfo.getBackupId() + " at "
                    + backupInfo.getTargetRootDir() + " failed due to " + e1.getMessage() + ".");
        }
    }

    /**
     * Fail the overall backup.
     * @param backupInfo backup context
     * @param e exception
     * @throws Exception exception
     */
    static void failBackup(final MasterProcedureEnv env, BackupInfo backupInfo, BackupManager backupManager,
            Exception e, String msg, BackupType type, Configuration conf) throws IOException {
        LOG.error(msg + getMessage(e), e);
        // If this is a cancel exception, then we've already cleaned.

        // set the failure timestamp of the overall backup
        backupInfo.setEndTs(EnvironmentEdgeManager.currentTime());

        // set failure message
        backupInfo.setFailedMsg(e.getMessage());

        // set overall backup status: failed
        backupInfo.setState(BackupState.FAILED);

        // compose the backup failed data
        String backupFailedData = "BackupId=" + backupInfo.getBackupId() + ",startts=" + backupInfo.getStartTs()
                + ",failedts=" + backupInfo.getEndTs() + ",failedphase=" + backupInfo.getPhase() + ",failedmessage="
                + backupInfo.getFailedMsg();
        LOG.error(backupFailedData);

        backupManager.updateBackupInfo(backupInfo);

        // if full backup, then delete HBase snapshots if there already are snapshots taken
        // and also clean up export snapshot log files if exist
        if (type == BackupType.FULL) {
            deleteSnapshot(env, backupInfo, conf);
            cleanupExportSnapshotLog(conf);
        }

        // clean up the uncompleted data at target directory if the ongoing backup has already entered
        // the copy phase
        // For incremental backup, DistCp logs will be cleaned with the targetDir.
        cleanupTargetDir(backupInfo, conf);

        LOG.info("Backup " + backupInfo.getBackupId() + " failed.");
    }

    /**
     * Do snapshot copy.
     * @param backupInfo backup context
     * @throws Exception exception
     */
    private void snapshotCopy(BackupInfo backupInfo) throws Exception {
        LOG.info("Snapshot copy is starting.");

        // set overall backup phase: snapshot_copy
        backupInfo.setPhase(BackupPhase.SNAPSHOTCOPY);

        // call ExportSnapshot to copy files based on hbase snapshot for backup
        // ExportSnapshot only support single snapshot export, need loop for multiple tables case
        BackupCopyService copyService = BackupRestoreServerFactory.getBackupCopyService(conf);

        // number of snapshots matches number of tables
        float numOfSnapshots = backupInfo.getSnapshotNames().size();

        LOG.debug("There are " + (int) numOfSnapshots + " snapshots to be copied.");

        for (TableName table : backupInfo.getTables()) {
            // Currently we simply set the sub copy tasks by counting the table snapshot number, we can
            // calculate the real files' size for the percentage in the future.
            // backupCopier.setSubTaskPercntgInWholeTask(1f / numOfSnapshots);
            int res = 0;
            String[] args = new String[4];
            args[0] = "-snapshot";
            args[1] = backupInfo.getSnapshotName(table);
            args[2] = "-copy-to";
            args[3] = backupInfo.getBackupStatus(table).getTargetDir();

            LOG.debug("Copy snapshot " + args[1] + " to " + args[3]);
            res = copyService.copy(backupInfo, backupManager, conf, BackupCopyService.Type.FULL, args);
            // if one snapshot export failed, do not continue for remained snapshots
            if (res != 0) {
                LOG.error("Exporting Snapshot " + args[1] + " failed with return code: " + res + ".");

                throw new IOException(
                        "Failed of exporting snapshot " + args[1] + " to " + args[3] + " with reason code " + res);
            }

            LOG.info("Snapshot copy " + args[1] + " finished.");
        }
    }

    /**
     * Add manifest for the current backup. The manifest is stored
     * within the table backup directory.
     * @param backupInfo The current backup context
     * @throws IOException exception
     * @throws BackupException exception
     */
    private static void addManifest(BackupInfo backupInfo, BackupManager backupManager, BackupType type,
            Configuration conf) throws IOException, BackupException {
        // set the overall backup phase : store manifest
        backupInfo.setPhase(BackupPhase.STORE_MANIFEST);

        BackupManifest manifest;

        // Since we have each table's backup in its own directory structure,
        // we'll store its manifest with the table directory.
        for (TableName table : backupInfo.getTables()) {
            manifest = new BackupManifest(backupInfo, table);
            ArrayList<BackupImage> ancestors = backupManager.getAncestors(backupInfo, table);
            for (BackupImage image : ancestors) {
                manifest.addDependentImage(image);
            }

            if (type == BackupType.INCREMENTAL) {
                // We'll store the log timestamps for this table only in its manifest.
                HashMap<TableName, HashMap<String, Long>> tableTimestampMap = new HashMap<TableName, HashMap<String, Long>>();
                tableTimestampMap.put(table, backupInfo.getIncrTimestampMap().get(table));
                manifest.setIncrTimestampMap(tableTimestampMap);
                ArrayList<BackupImage> ancestorss = backupManager.getAncestors(backupInfo);
                for (BackupImage image : ancestorss) {
                    manifest.addDependentImage(image);
                }
            }
            manifest.store(conf);
        }

        // For incremental backup, we store a overall manifest in
        // <backup-root-dir>/WALs/<backup-id>
        // This is used when created the next incremental backup
        if (type == BackupType.INCREMENTAL) {
            manifest = new BackupManifest(backupInfo);
            // set the table region server start and end timestamps for incremental backup
            manifest.setIncrTimestampMap(backupInfo.getIncrTimestampMap());
            ArrayList<BackupImage> ancestors = backupManager.getAncestors(backupInfo);
            for (BackupImage image : ancestors) {
                manifest.addDependentImage(image);
            }
            manifest.store(conf);
        }
    }

    /**
     * Get backup request meta data dir as string.
     * @param backupInfo backup context
     * @return meta data dir
     */
    private static String obtainBackupMetaDataStr(BackupInfo backupInfo) {
        StringBuffer sb = new StringBuffer();
        sb.append("type=" + backupInfo.getType() + ",tablelist=");
        for (TableName table : backupInfo.getTables()) {
            sb.append(table + ";");
        }
        if (sb.lastIndexOf(";") > 0) {
            sb.delete(sb.lastIndexOf(";"), sb.lastIndexOf(";") + 1);
        }
        sb.append(",targetRootDir=" + backupInfo.getTargetRootDir());

        return sb.toString();
    }

    /**
     * Clean up directories with prefix "_distcp_logs-", which are generated when DistCp copying
     * hlogs.
     * @throws IOException exception
     */
    private static void cleanupDistCpLog(BackupInfo backupInfo, Configuration conf) throws IOException {
        Path rootPath = new Path(backupInfo.getHLogTargetDir()).getParent();
        FileSystem fs = FileSystem.get(rootPath.toUri(), conf);
        FileStatus[] files = FSUtils.listStatus(fs, rootPath);
        if (files == null) {
            return;
        }
        for (FileStatus file : files) {
            if (file.getPath().getName().startsWith("_distcp_logs")) {
                LOG.debug("Delete log files of DistCp: " + file.getPath().getName());
                FSUtils.delete(fs, file.getPath(), true);
            }
        }
    }

    /**
     * Complete the overall backup.
     * @param backupInfo backup context
     * @throws Exception exception
     */
    static void completeBackup(final MasterProcedureEnv env, BackupInfo backupInfo, BackupManager backupManager,
            BackupType type, Configuration conf) throws IOException {
        // set the complete timestamp of the overall backup
        backupInfo.setEndTs(EnvironmentEdgeManager.currentTime());
        // set overall backup status: complete
        backupInfo.setState(BackupState.COMPLETE);
        backupInfo.setProgress(100);
        // add and store the manifest for the backup
        addManifest(backupInfo, backupManager, type, conf);

        // after major steps done and manifest persisted, do convert if needed for incremental backup
        /* in-fly convert code here, provided by future jira */
        LOG.debug("in-fly convert code here, provided by future jira");

        // compose the backup complete data
        String backupCompleteData = obtainBackupMetaDataStr(backupInfo) + ",startts=" + backupInfo.getStartTs()
                + ",completets=" + backupInfo.getEndTs() + ",bytescopied=" + backupInfo.getTotalBytesCopied();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Backup " + backupInfo.getBackupId() + " finished: " + backupCompleteData);
        }
        backupManager.updateBackupInfo(backupInfo);

        // when full backup is done:
        // - delete HBase snapshot
        // - clean up directories with prefix "exportSnapshot-", which are generated when exporting
        // snapshots
        if (type == BackupType.FULL) {
            deleteSnapshot(env, backupInfo, conf);
            cleanupExportSnapshotLog(conf);
        } else if (type == BackupType.INCREMENTAL) {
            cleanupDistCpLog(backupInfo, conf);
        }

        LOG.info("Backup " + backupInfo.getBackupId() + " completed.");
    }

    /**
     * Wrap a SnapshotDescription for a target table.
     * @param table table
     * @return a SnapshotDescription especially for backup.
     */
    static SnapshotDescription wrapSnapshotDescription(TableName tableName, String snapshotName) {
        // Mock a SnapshotDescription from backupInfo to call SnapshotManager function,
        // Name it in the format "snapshot_<timestamp>_<table>"
        HBaseProtos.SnapshotDescription.Builder builder = HBaseProtos.SnapshotDescription.newBuilder();
        builder.setTable(tableName.getNameAsString());
        builder.setName(snapshotName);
        HBaseProtos.SnapshotDescription backupSnapshot = builder.build();

        LOG.debug("Wrapped a SnapshotDescription " + backupSnapshot.getName()
                + " from backupInfo to request snapshot for backup.");

        return backupSnapshot;
    }

    @Override
    protected Flow executeFromState(final MasterProcedureEnv env, final FullTableBackupState state) {
        if (conf == null) {
            conf = env.getMasterConfiguration();
        }
        if (backupManager == null) {
            try {
                backupManager = new BackupManager(env.getMasterConfiguration());
            } catch (IOException ioe) {
                setFailure("full backup", ioe);
                return Flow.NO_MORE_STATE;
            }
        }
        if (LOG.isTraceEnabled()) {
            LOG.trace(this + " execute state=" + state);
        }
        try {
            switch (state) {
            case PRE_SNAPSHOT_TABLE:
                beginBackup(backupManager, backupInfo);
                String savedStartCode = null;
                boolean firstBackup = false;
                // do snapshot for full table backup

                try {
                    savedStartCode = backupManager.readBackupStartCode();
                    firstBackup = savedStartCode == null || Long.parseLong(savedStartCode) == 0L;
                    if (firstBackup) {
                        // This is our first backup. Let's put some marker on ZK so that we can hold the logs
                        // while we do the backup.
                        backupManager.writeBackupStartCode(0L);
                    }
                    // We roll log here before we do the snapshot. It is possible there is duplicate data
                    // in the log that is already in the snapshot. But if we do it after the snapshot, we
                    // could have data loss.
                    // A better approach is to do the roll log on each RS in the same global procedure as
                    // the snapshot.
                    LOG.info("Execute roll log procedure for full backup ...");
                    MasterProcedureManager mpm = env.getMasterServices().getMasterProcedureManagerHost()
                            .getProcedureManager(LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_SIGNATURE);
                    Map<String, String> props = new HashMap<String, String>();
                    props.put("backupRoot", backupInfo.getTargetRootDir());
                    long waitTime = MasterProcedureUtil.execProcedure(mpm,
                            LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_SIGNATURE,
                            LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_NAME, props);
                    MasterProcedureUtil.waitForProcedure(mpm,
                            LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_SIGNATURE,
                            LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_NAME, props, waitTime,
                            conf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
                                    HConstants.DEFAULT_HBASE_CLIENT_RETRIES_NUMBER),
                            conf.getLong(HConstants.HBASE_CLIENT_PAUSE, HConstants.DEFAULT_HBASE_CLIENT_PAUSE));

                    newTimestamps = backupManager.readRegionServerLastLogRollResult();
                    if (firstBackup) {
                        // Updates registered log files
                        // We record ALL old WAL files as registered, because
                        // this is a first full backup in the system and these
                        // files are not needed for next incremental backup
                        List<String> logFiles = BackupServerUtil.getWALFilesOlderThan(conf, newTimestamps);
                        backupManager.recordWALFiles(logFiles);
                    }
                } catch (BackupException e) {
                    setFailure("Failure in full-backup: pre-snapshot phase", e);
                    // fail the overall backup and return
                    failBackup(env, backupInfo, backupManager, e, "Unexpected BackupException : ", BackupType.FULL,
                            conf);
                    return Flow.NO_MORE_STATE;
                }
                setNextState(FullTableBackupState.SNAPSHOT_TABLES);
                break;
            case SNAPSHOT_TABLES:
                for (TableName tableName : tableList) {
                    String snapshotName = "snapshot_" + Long.toString(EnvironmentEdgeManager.currentTime()) + "_"
                            + tableName.getNamespaceAsString() + "_" + tableName.getQualifierAsString();
                    HBaseProtos.SnapshotDescription backupSnapshot;

                    // wrap a SnapshotDescription for offline/online snapshot
                    backupSnapshot = wrapSnapshotDescription(tableName, snapshotName);
                    try {
                        env.getMasterServices().getSnapshotManager().deleteSnapshot(backupSnapshot);
                    } catch (IOException e) {
                        LOG.debug("Unable to delete " + snapshotName, e);
                    }
                    // Kick off snapshot for backup
                    snapshotTable(env, backupSnapshot);
                    backupInfo.setSnapshotName(tableName, backupSnapshot.getName());
                }
                setNextState(FullTableBackupState.SNAPSHOT_COPY);
                break;
            case SNAPSHOT_COPY:
                // do snapshot copy
                LOG.debug("snapshot copy for " + backupId);
                try {
                    this.snapshotCopy(backupInfo);
                } catch (Exception e) {
                    setFailure("Failure in full-backup: snapshot copy phase" + backupId, e);
                    // fail the overall backup and return
                    failBackup(env, backupInfo, backupManager, e, "Unexpected BackupException : ", BackupType.FULL,
                            conf);
                    return Flow.NO_MORE_STATE;
                }
                // Updates incremental backup table set
                backupManager.addIncrementalBackupTableSet(backupInfo.getTables());
                setNextState(FullTableBackupState.BACKUP_COMPLETE);
                break;

            case BACKUP_COMPLETE:
                // set overall backup status: complete. Here we make sure to complete the backup.
                // After this checkpoint, even if entering cancel process, will let the backup finished
                backupInfo.setState(BackupState.COMPLETE);
                // The table list in backupInfo is good for both full backup and incremental backup.
                // For incremental backup, it contains the incremental backup table set.
                backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps);

                HashMap<TableName, HashMap<String, Long>> newTableSetTimestampMap = backupManager
                        .readLogTimestampMap();

                Long newStartCode = BackupClientUtil
                        .getMinValue(BackupServerUtil.getRSLogTimestampMins(newTableSetTimestampMap));
                backupManager.writeBackupStartCode(newStartCode);

                // backup complete
                completeBackup(env, backupInfo, backupManager, BackupType.FULL, conf);
                return Flow.NO_MORE_STATE;

            default:
                throw new UnsupportedOperationException("unhandled state=" + state);
            }
        } catch (IOException e) {
            LOG.error("Backup failed in " + state);
            setFailure("snapshot-table", e);
        }
        return Flow.HAS_MORE_STATE;
    }

    private void snapshotTable(final MasterProcedureEnv env, SnapshotDescription backupSnapshot)
            throws IOException {

        int maxAttempts = env.getMasterConfiguration().getInt(SNAPSHOT_BACKUP_MAX_ATTEMPTS_KEY,
                DEFAULT_SNAPSHOT_BACKUP_MAX_ATTEMPTS);
        int delay = env.getMasterConfiguration().getInt(SNAPSHOT_BACKUP_ATTEMPTS_DELAY_KEY,
                DEFAULT_SNAPSHOT_BACKUP_ATTEMPTS_DELAY);
        int attempts = 0;

        while (attempts++ < maxAttempts) {
            try {
                env.getMasterServices().getSnapshotManager().takeSnapshot(backupSnapshot);
                long waitTime = SnapshotDescriptionUtils.getMaxMasterTimeout(env.getMasterConfiguration(),
                        backupSnapshot.getType(), SnapshotDescriptionUtils.DEFAULT_MAX_WAIT_TIME);
                BackupServerUtil.waitForSnapshot(backupSnapshot, waitTime,
                        env.getMasterServices().getSnapshotManager(), env.getMasterConfiguration());
                break;
            } catch (NotServingRegionException ee) {
                LOG.warn("Snapshot attempt " + attempts + " failed for table " + backupSnapshot.getTable()
                        + ", sleeping for " + delay + "ms", ee);
                if (attempts < maxAttempts) {
                    try {
                        Thread.sleep(delay);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }

    @Override
    protected void rollbackState(final MasterProcedureEnv env, final FullTableBackupState state)
            throws IOException {
        if (state != FullTableBackupState.PRE_SNAPSHOT_TABLE) {
            deleteSnapshot(env, backupInfo, conf);
            cleanupExportSnapshotLog(conf);
        }

        // clean up the uncompleted data at target directory if the ongoing backup has already entered
        // the copy phase
        // For incremental backup, DistCp logs will be cleaned with the targetDir.
        if (state == FullTableBackupState.SNAPSHOT_COPY) {
            cleanupTargetDir(backupInfo, conf);
        }
    }

    @Override
    protected FullTableBackupState getState(final int stateId) {
        return FullTableBackupState.valueOf(stateId);
    }

    @Override
    protected int getStateId(final FullTableBackupState state) {
        return state.getNumber();
    }

    @Override
    protected FullTableBackupState getInitialState() {
        return FullTableBackupState.PRE_SNAPSHOT_TABLE;
    }

    @Override
    protected void setNextState(final FullTableBackupState state) {
        if (aborted.get()) {
            setAbortFailure("backup-table", "abort requested");
        } else {
            super.setNextState(state);
        }
    }

    @Override
    public boolean abort(final MasterProcedureEnv env) {
        aborted.set(true);
        return true;
    }

    @Override
    public void toStringClassDetails(StringBuilder sb) {
        sb.append(getClass().getSimpleName());
        sb.append(" (targetRootDir=");
        sb.append(targetRootDir);
        sb.append("; backupId=").append(backupId);
        sb.append("; tables=");
        int len = tableList.size();
        for (int i = 0; i < len - 1; i++) {
            sb.append(tableList.get(i)).append(",");
        }
        sb.append(tableList.get(len - 1));
        sb.append(")");
    }

    BackupProtos.BackupProcContext toBackupInfo() {
        BackupProtos.BackupProcContext.Builder ctxBuilder = BackupProtos.BackupProcContext.newBuilder();
        ctxBuilder.setCtx(backupInfo.toProtosBackupInfo());
        if (newTimestamps != null && !newTimestamps.isEmpty()) {
            BackupProtos.ServerTimestamp.Builder tsBuilder = ServerTimestamp.newBuilder();
            for (Entry<String, Long> entry : newTimestamps.entrySet()) {
                tsBuilder.clear().setServer(entry.getKey()).setTimestamp(entry.getValue());
                ctxBuilder.addServerTimestamp(tsBuilder.build());
            }
        }
        return ctxBuilder.build();
    }

    @Override
    public void serializeStateData(final OutputStream stream) throws IOException {
        super.serializeStateData(stream);

        BackupProtos.BackupProcContext backupProcCtx = toBackupInfo();
        backupProcCtx.writeDelimitedTo(stream);
    }

    @Override
    public void deserializeStateData(final InputStream stream) throws IOException {
        super.deserializeStateData(stream);

        BackupProtos.BackupProcContext proto = BackupProtos.BackupProcContext.parseDelimitedFrom(stream);
        backupInfo = BackupInfo.fromProto(proto.getCtx());
        backupId = backupInfo.getBackupId();
        targetRootDir = backupInfo.getTargetRootDir();
        tableList = backupInfo.getTableNames();
        List<ServerTimestamp> svrTimestamps = proto.getServerTimestampList();
        if (svrTimestamps != null && !svrTimestamps.isEmpty()) {
            newTimestamps = new HashMap<>();
            for (ServerTimestamp ts : svrTimestamps) {
                newTimestamps.put(ts.getServer(), ts.getTimestamp());
            }
        }
    }

    @Override
    public TableName getTableName() {
        return TableName.BACKUP_TABLE_NAME;
    }

    @Override
    public TableOperationType getTableOperationType() {
        return TableOperationType.BACKUP;
    }

    @Override
    protected boolean acquireLock(final MasterProcedureEnv env) {
        if (!env.isInitialized() && !getTableName().isSystemTable()) {
            return false;
        }
        return env.getProcedureQueue().tryAcquireTableWrite(getTableName(), "full backup");
        /*
        if (env.waitInitialized(this)) {
          return false;
        }
        return env.getProcedureQueue().tryAcquireTableExclusiveLock(this, TableName.BACKUP_TABLE_NAME);
        */
    }

    @Override
    protected void releaseLock(final MasterProcedureEnv env) {
        env.getProcedureQueue().releaseTableWrite(getTableName());
    }
}