001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.backup.impl;
019
020import static org.apache.hadoop.hbase.backup.BackupInfo.withState;
021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR;
022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.JOB_NAME_CONF_KEY;
023import static org.apache.hadoop.hbase.backup.mapreduce.MapReduceBackupCopyJob.NUMBER_OF_LEVELS_TO_PRESERVE_KEY;
024import static org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2.MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY;
025
026import java.io.IOException;
027import java.net.URI;
028import java.net.URISyntaxException;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.stream.Collectors;
036import org.apache.commons.io.FilenameUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.hadoop.conf.Configuration;
039import org.apache.hadoop.fs.FileSystem;
040import org.apache.hadoop.fs.LocatedFileStatus;
041import org.apache.hadoop.fs.Path;
042import org.apache.hadoop.fs.RemoteIterator;
043import org.apache.hadoop.hbase.TableName;
044import org.apache.hadoop.hbase.backup.BackupCopyJob;
045import org.apache.hadoop.hbase.backup.BackupInfo;
046import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase;
047import org.apache.hadoop.hbase.backup.BackupRequest;
048import org.apache.hadoop.hbase.backup.BackupRestoreFactory;
049import org.apache.hadoop.hbase.backup.BackupType;
050import org.apache.hadoop.hbase.backup.HBackupFileSystem;
051import org.apache.hadoop.hbase.backup.mapreduce.MapReduceHFileSplitterJob;
052import org.apache.hadoop.hbase.backup.util.BackupUtils;
053import org.apache.hadoop.hbase.client.Admin;
054import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
055import org.apache.hadoop.hbase.client.Connection;
056import org.apache.hadoop.hbase.io.hfile.HFile;
057import org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2;
058import org.apache.hadoop.hbase.mapreduce.WALInputFormat;
059import org.apache.hadoop.hbase.mapreduce.WALPlayer;
060import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
061import org.apache.hadoop.hbase.snapshot.SnapshotManifest;
062import org.apache.hadoop.hbase.snapshot.SnapshotRegionLocator;
063import org.apache.hadoop.hbase.snapshot.SnapshotTTLExpiredException;
064import org.apache.hadoop.hbase.util.CommonFSUtils;
065import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
066import org.apache.hadoop.hbase.util.HFileArchiveUtil;
067import org.apache.hadoop.hbase.wal.AbstractFSWALProvider;
068import org.apache.hadoop.util.Tool;
069import org.apache.yetus.audience.InterfaceAudience;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073import org.apache.hbase.thirdparty.com.google.common.base.Strings;
074import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
075
076import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
077import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos;
078
079/**
080 * Incremental backup implementation. See the {@link #execute() execute} method.
081 */
082@InterfaceAudience.Private
083public class IncrementalTableBackupClient extends TableBackupClient {
084  private static final Logger LOG = LoggerFactory.getLogger(IncrementalTableBackupClient.class);
085  private static final String BULKLOAD_COLLECTOR_OUTPUT = "bulkload-collector-output";
086
087  protected IncrementalTableBackupClient() {
088  }
089
090  public IncrementalTableBackupClient(final Connection conn, final String backupId,
091    BackupRequest request) throws IOException {
092    super(conn, backupId, request);
093  }
094
095  protected List<String> filterMissingFiles(List<String> incrBackupFileList) throws IOException {
096    List<String> list = new ArrayList<>();
097    for (String file : incrBackupFileList) {
098      Path p = new Path(file);
099      if (fs.exists(p) || isActiveWalPath(p)) {
100        list.add(file);
101      } else {
102        LOG.warn("Can't find file: " + file);
103      }
104    }
105    return list;
106  }
107
108  /**
109   * Check if a given path belongs to active WAL directory
110   * @param p path
111   * @return true, if yes
112   */
113  protected boolean isActiveWalPath(Path p) {
114    return !AbstractFSWALProvider.isArchivedLogFile(p);
115  }
116
117  protected static int getIndex(TableName tbl, List<TableName> sTableList) {
118    if (sTableList == null) {
119      return 0;
120    }
121
122    for (int i = 0; i < sTableList.size(); i++) {
123      if (tbl.equals(sTableList.get(i))) {
124        return i;
125      }
126    }
127    return -1;
128  }
129
130  /**
131   * Reads bulk load records from backup table, iterates through the records and forms the paths for
132   * bulk loaded hfiles. Copies the bulk loaded hfiles to the backup destination. This method does
133   * NOT clean up the entries in the bulk load system table. Those entries should not be cleaned
134   * until the backup is marked as complete.
135   * @param tablesToBackup list of tables to be backed up
136   */
137  protected List<BulkLoad> handleBulkLoad(List<TableName> tablesToBackup,
138    Map<TableName, List<String>> tablesToWALFileList, Map<TableName, Long> tablesToPrevBackupTs)
139    throws IOException {
140    Map<TableName, MergeSplitBulkloadInfo> toBulkload = new HashMap<>();
141    List<BulkLoad> bulkLoads = new ArrayList<>();
142
143    FileSystem tgtFs;
144    try {
145      tgtFs = FileSystem.get(new URI(backupInfo.getBackupRootDir()), conf);
146    } catch (URISyntaxException use) {
147      throw new IOException("Unable to get FileSystem", use);
148    }
149
150    Path rootdir = CommonFSUtils.getRootDir(conf);
151    Path tgtRoot = new Path(new Path(backupInfo.getBackupRootDir()), backupId);
152
153    if (!backupInfo.isContinuousBackupEnabled()) {
154      bulkLoads = backupManager.readBulkloadRows(tablesToBackup);
155      for (BulkLoad bulkLoad : bulkLoads) {
156        TableName srcTable = bulkLoad.getTableName();
157        if (!tablesToBackup.contains(srcTable)) {
158          LOG.debug("Skipping {} since it is not in tablesToBackup", srcTable);
159          continue;
160        }
161
162        MergeSplitBulkloadInfo bulkloadInfo =
163          toBulkload.computeIfAbsent(srcTable, MergeSplitBulkloadInfo::new);
164        String regionName = bulkLoad.getRegion();
165        String fam = bulkLoad.getColumnFamily();
166        String filename = FilenameUtils.getName(bulkLoad.getHfilePath());
167        Path tblDir = CommonFSUtils.getTableDir(rootdir, srcTable);
168        Path p = new Path(tblDir, regionName + Path.SEPARATOR + fam + Path.SEPARATOR + filename);
169        String srcTableQualifier = srcTable.getQualifierAsString();
170        String srcTableNs = srcTable.getNamespaceAsString();
171        Path tgtFam = new Path(tgtRoot, srcTableNs + Path.SEPARATOR + srcTableQualifier
172          + Path.SEPARATOR + regionName + Path.SEPARATOR + fam);
173        if (!tgtFs.mkdirs(tgtFam)) {
174          throw new IOException("couldn't create " + tgtFam);
175        }
176
177        Path tgt = new Path(tgtFam, filename);
178        Path archiveDir = HFileArchiveUtil.getStoreArchivePath(conf, srcTable, regionName, fam);
179        Path archive = new Path(archiveDir, filename);
180
181        if (fs.exists(p)) {
182          if (LOG.isTraceEnabled()) {
183            LOG.trace("found bulk hfile {} in {} for {}", bulkLoad.getHfilePath(), p.getParent(),
184              srcTableQualifier);
185            LOG.trace("copying {} to {}", p, tgt);
186          }
187          bulkloadInfo.addActiveFile(p.toString());
188        } else if (fs.exists(archive)) {
189          LOG.debug("copying archive {} to {}", archive, tgt);
190          bulkloadInfo.addArchiveFiles(archive.toString());
191        }
192      }
193
194      for (MergeSplitBulkloadInfo bulkloadInfo : toBulkload.values()) {
195        mergeSplitAndCopyBulkloadedHFiles(bulkloadInfo.getActiveFiles(),
196          bulkloadInfo.getArchiveFiles(), bulkloadInfo.getSrcTable(), tgtFs);
197      }
198    } else {
199      // Continuous incremental backup: run BulkLoadCollectorJob over backed-up WALs
200      Path collectorOutput = new Path(getBulkOutputDir(), BULKLOAD_COLLECTOR_OUTPUT);
201      for (TableName table : tablesToBackup) {
202        long startTs = tablesToPrevBackupTs.getOrDefault(table, 0L);
203        long endTs = backupInfo.getIncrCommittedWalTs();
204        List<String> walDirs = tablesToWALFileList.getOrDefault(table, new ArrayList<String>());
205
206        List<Path> bulkloadPaths = BackupUtils.collectBulkFiles(conn, table, table, startTs, endTs,
207          collectorOutput, walDirs);
208
209        List<String> bulkLoadFiles =
210          bulkloadPaths.stream().map(Path::toString).collect(Collectors.toList());
211
212        if (bulkLoadFiles.isEmpty()) {
213          LOG.info("No bulk-load files found for table {}", table);
214          continue;
215        }
216
217        mergeSplitAndCopyBulkloadedHFiles(bulkLoadFiles, table, tgtFs);
218      }
219    }
220    return bulkLoads;
221  }
222
223  private void mergeSplitAndCopyBulkloadedHFiles(List<String> activeFiles,
224    List<String> archiveFiles, TableName tn, FileSystem tgtFs) throws IOException {
225    int attempt = 1;
226
227    while (!activeFiles.isEmpty()) {
228      LOG.info("MergeSplit {} active bulk loaded files. Attempt={}", activeFiles.size(), attempt++);
229      // Active file can be archived during copy operation,
230      // we need to handle this properly
231      try {
232        mergeSplitAndCopyBulkloadedHFiles(activeFiles, tn, tgtFs);
233        break;
234      } catch (IOException e) {
235        int numActiveFiles = activeFiles.size();
236        updateFileLists(activeFiles, archiveFiles);
237        if (activeFiles.size() < numActiveFiles) {
238          // We've archived some files, delete bulkloads directory
239          // and re-try
240          deleteBulkLoadDirectory();
241          continue;
242        }
243
244        throw e;
245      }
246    }
247
248    if (!archiveFiles.isEmpty()) {
249      mergeSplitAndCopyBulkloadedHFiles(archiveFiles, tn, tgtFs);
250    }
251  }
252
253  private void mergeSplitAndCopyBulkloadedHFiles(List<String> files, TableName tn, FileSystem tgtFs)
254    throws IOException {
255    MapReduceHFileSplitterJob player = new MapReduceHFileSplitterJob();
256    Configuration conf = new Configuration(this.conf);
257    conf.set(MapReduceHFileSplitterJob.BULK_OUTPUT_CONF_KEY,
258      getBulkOutputDirForTable(tn).toString());
259    if (backupInfo.isContinuousBackupEnabled()) {
260      conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false);
261    }
262    player.setConf(conf);
263
264    String inputDirs = StringUtils.join(files, ",");
265    String[] args = { inputDirs, tn.getNameWithNamespaceInclAsString() };
266
267    int result;
268
269    try {
270      result = player.run(args);
271    } catch (Exception e) {
272      LOG.error("Failed to run MapReduceHFileSplitterJob", e);
273      // Delete the bulkload directory if we fail to run the HFile splitter job for any reason
274      // as it might be re-tried
275      deleteBulkLoadDirectory();
276      throw new IOException(e);
277    }
278
279    if (result != 0) {
280      throw new IOException(
281        "Failed to run MapReduceHFileSplitterJob with invalid result: " + result);
282    }
283
284    incrementalCopyBulkloadHFiles(tgtFs, tn);
285  }
286
287  public void updateFileLists(List<String> activeFiles, List<String> archiveFiles)
288    throws IOException {
289    List<String> newlyArchived = new ArrayList<>();
290
291    for (String spath : activeFiles) {
292      if (!fs.exists(new Path(spath))) {
293        newlyArchived.add(spath);
294      }
295    }
296
297    if (!newlyArchived.isEmpty()) {
298      String rootDir = CommonFSUtils.getRootDir(conf).toString();
299
300      activeFiles.removeAll(newlyArchived);
301      for (String file : newlyArchived) {
302        String archivedFile = file.substring(rootDir.length() + 1);
303        Path archivedFilePath = new Path(HFileArchiveUtil.getArchivePath(conf), archivedFile);
304        archivedFile = archivedFilePath.toString();
305
306        if (!fs.exists(archivedFilePath)) {
307          throw new IOException(String.format(
308            "File %s no longer exists, and no archived file %s exists for it", file, archivedFile));
309        }
310
311        LOG.debug("Archived file {} has been updated", archivedFile);
312        archiveFiles.add(archivedFile);
313      }
314    }
315
316    LOG.debug(newlyArchived.size() + " files have been archived.");
317  }
318
319  /**
320   * @throws IOException                   If the execution of the backup fails
321   * @throws ColumnFamilyMismatchException If the column families of the current table do not match
322   *                                       the column families for the last full backup. In which
323   *                                       case, a full backup should be taken
324   */
325  @Override
326  public void execute() throws IOException, ColumnFamilyMismatchException {
327    // tablesToWALFileList and tablesToPrevBackupTs are needed for "continuous" Incremental backup
328    Map<TableName, List<String>> tablesToWALFileList = new HashMap<>();
329    Map<TableName, Long> tablesToPrevBackupTs = new HashMap<>();
330    try {
331      Map<TableName, String> tablesToFullBackupIds = getFullBackupIds();
332      verifyCfCompatibility(backupInfo.getTables(), tablesToFullBackupIds);
333
334      // case PREPARE_INCREMENTAL:
335      if (backupInfo.isContinuousBackupEnabled()) {
336        // committedWALsTs is needed only for Incremental backups with continuous backup
337        // since these do not depend on log roll ts
338        long committedWALsTs = BackupUtils.getReplicationCheckpoint(conn);
339        backupInfo.setIncrCommittedWalTs(committedWALsTs);
340      }
341      beginBackup(backupManager, backupInfo);
342      backupInfo.setPhase(BackupPhase.PREPARE_INCREMENTAL);
343      // Non-continuous Backup incremental backup is controlled by 'incremental backup table set'
344      // and not by user provided backup table list. This is an optimization to avoid copying
345      // the same set of WALs for incremental backups of different tables at different times
346      // HBASE-14038
347      // Continuous-incremental backup backs up user provided table list/set
348      Set<TableName> currentTableSet;
349      if (backupInfo.isContinuousBackupEnabled()) {
350        currentTableSet = backupInfo.getTables();
351      } else {
352        currentTableSet = backupManager.getIncrementalBackupTableSet();
353        newTimestamps = ((IncrementalBackupManager) backupManager).getIncrBackupLogFileMap();
354      }
355      LOG.debug("For incremental backup, the current table set is {}", currentTableSet);
356    } catch (Exception e) {
357      // fail the overall backup and return
358      failBackup(conn, backupInfo, backupManager, e, "Unexpected Exception : ",
359        BackupType.INCREMENTAL, conf);
360      throw new IOException(e);
361    }
362
363    // case INCREMENTAL_COPY:
364    try {
365      // copy out the table and region info files for each table
366      BackupUtils.copyTableRegionInfo(conn, backupInfo, conf);
367      setupRegionLocator();
368      // convert WAL to HFiles and copy them to .tmp under BACKUP_ROOT
369      convertWALsToHFiles(tablesToWALFileList, tablesToPrevBackupTs);
370
371      String[] bulkOutputFiles;
372      String backupDest = backupInfo.getBackupRootDir();
373      if (backupInfo.isContinuousBackupEnabled()) {
374        // For the continuous backup case, the WALs have been converted to HFiles in a separate
375        // map-reduce job for each table. In order to prevent MR job failures due to HBASE-29891,
376        // these HFiles were sent to a different output directory for each table. This means
377        // continuous backups require a list of source directories and a different destination
378        // directory when copying HFiles to the incremental backup directory.
379        List<String> uniqueNamespaces = tablesToWALFileList.keySet().stream()
380          .map(TableName::getNamespaceAsString).distinct().toList();
381        bulkOutputFiles = uniqueNamespaces.stream()
382          .map(ns -> new Path(getBulkOutputDir(), ns).toString()).toArray(String[]::new);
383        backupDest = backupDest + Path.SEPARATOR + backupId;
384      } else {
385        bulkOutputFiles = new String[] { getBulkOutputDir().toString() };
386      }
387      incrementalCopyHFiles(bulkOutputFiles, backupDest);
388    } catch (Exception e) {
389      String msg = "Unexpected exception in incremental-backup: incremental copy " + backupId + " ";
390      // fail the overall backup and return
391      failBackup(conn, backupInfo, backupManager, e, msg, BackupType.INCREMENTAL, conf);
392      throw new IOException(e);
393    }
394    // case INCR_BACKUP_COMPLETE:
395    // set overall backup status: complete. Here we make sure to complete the backup.
396    // After this checkpoint, even if entering cancel process, will let the backup finished
397    try {
398      if (!backupInfo.isContinuousBackupEnabled()) {
399        // Set the previousTimestampMap which is before this current log roll to the manifest.
400        Map<TableName, Map<String, Long>> previousTimestampMap =
401          backupManager.readLogTimestampMap();
402        backupInfo.setIncrTimestampMap(previousTimestampMap);
403
404        // The table list in backupInfo is good for both full backup and incremental backup.
405        // For incremental backup, it contains the incremental backup table set.
406        backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps);
407      }
408
409      Map<TableName, Map<String, Long>> newTableSetTimestampMap =
410        backupManager.readLogTimestampMap();
411
412      List<BulkLoad> bulkLoads =
413        handleBulkLoad(backupInfo.getTableNames(), tablesToWALFileList, tablesToPrevBackupTs);
414
415      // backup complete
416      backupInfo.setTableSetTimestampMap(newTableSetTimestampMap);
417      completeBackup(conn, backupInfo, BackupType.INCREMENTAL, conf);
418
419      List<byte[]> bulkLoadedRows = Lists.transform(bulkLoads, BulkLoad::getRowKey);
420      backupManager.deleteBulkLoadedRows(bulkLoadedRows);
421    } catch (IOException e) {
422      failBackup(conn, backupInfo, backupManager, e, "Unexpected Exception : ",
423        BackupType.INCREMENTAL, conf);
424      throw new IOException(e);
425    } finally {
426      if (backupInfo.isContinuousBackupEnabled()) {
427        deleteTmpBackupDirectory();
428      }
429    }
430  }
431
432  protected void incrementalCopyHFiles(String[] files, String backupDest) throws IOException {
433    boolean diskBasedSortingOriginalValue = HFileOutputFormat2.diskBasedSortingEnabled(conf);
434    try {
435      LOG.debug("Incremental copy HFiles is starting. dest={}", backupDest);
436      // set overall backup phase: incremental_copy
437      backupInfo.setPhase(BackupPhase.INCREMENTAL_COPY);
438      // get incremental backup file list and prepare parms for DistCp
439      String[] strArr = new String[files.length + 1];
440      System.arraycopy(files, 0, strArr, 0, files.length);
441      strArr[strArr.length - 1] = backupDest;
442
443      String jobname = "Incremental_Backup-HFileCopy-" + backupInfo.getBackupId() + "-"
444        + System.currentTimeMillis();
445      if (LOG.isDebugEnabled()) {
446        LOG.debug("Setting incremental copy HFiles job name to : " + jobname);
447      }
448      conf.set(JOB_NAME_CONF_KEY, jobname);
449      conf.setBoolean(HFileOutputFormat2.DISK_BASED_SORTING_ENABLED_KEY, true);
450
451      BackupCopyJob copyService = BackupRestoreFactory.getBackupCopyJob(conf);
452      int res = copyService.copy(backupInfo, backupManager, conf, BackupType.INCREMENTAL, strArr);
453      if (res != 0) {
454        LOG.error("Copy incremental HFile files failed with return code: " + res + ".");
455        throw new IOException(
456          "Failed copy from " + StringUtils.join(files, ',') + " to " + backupDest);
457      }
458      LOG.debug("Incremental copy HFiles from " + StringUtils.join(files, ',') + " to " + backupDest
459        + " finished.");
460    } finally {
461      deleteBulkLoadDirectory();
462      conf.setBoolean(HFileOutputFormat2.DISK_BASED_SORTING_ENABLED_KEY,
463        diskBasedSortingOriginalValue);
464    }
465  }
466
467  protected void deleteBulkLoadDirectory() throws IOException {
468    // delete original bulk load directory on method exit
469    Path path = getBulkOutputDir();
470    deleteDirectory(path);
471  }
472
473  protected void deleteTmpBackupDirectory() throws IOException {
474    Path path = getTmpBackupDir();
475    deleteDirectory(path);
476  }
477
478  private void deleteDirectory(Path path) throws IOException {
479    FileSystem fs = FileSystem.get(path.toUri(), conf);
480    boolean result = fs.delete(path, true);
481    if (!result) {
482      LOG.warn("Could not delete {}", path);
483    }
484  }
485
486  protected void convertWALsToHFiles(Map<TableName, List<String>> tablesToWALFileList,
487    Map<TableName, Long> tablesToPrevBackupTs) throws IOException {
488    long previousBackupTs = 0L;
489    long currentBackupTs = 0L;
490    if (backupInfo.isContinuousBackupEnabled()) {
491      String walBackupDir = conf.get(CONF_CONTINUOUS_BACKUP_WAL_DIR);
492      if (Strings.isNullOrEmpty(walBackupDir)) {
493        throw new IOException(
494          "Incremental backup requires the WAL backup directory " + CONF_CONTINUOUS_BACKUP_WAL_DIR);
495      }
496      Path walBackupPath = new Path(walBackupDir);
497      Set<TableName> tableSet = backupInfo.getTables();
498      currentBackupTs = backupInfo.getIncrCommittedWalTs();
499      List<BackupInfo> backupInfos =
500        backupManager.getBackupHistory(withState(BackupInfo.BackupState.COMPLETE));
501      for (TableName table : tableSet) {
502        for (BackupInfo backup : backupInfos) {
503          // find previous backup for this table
504          if (backup.getTables().contains(table)) {
505            LOG.info("Found previous backup of type {} with id {} for table {}", backup.getType(),
506              backup.getBackupId(), table.getNameAsString());
507            List<String> walBackupFileList;
508            if (backup.getType() == BackupType.FULL) {
509              previousBackupTs = backup.getStartTs();
510            } else {
511              previousBackupTs = backup.getIncrCommittedWalTs();
512            }
513            walBackupFileList =
514              BackupUtils.getValidWalDirs(conf, walBackupPath, previousBackupTs, currentBackupTs);
515            tablesToWALFileList.put(table, walBackupFileList);
516            tablesToPrevBackupTs.put(table, previousBackupTs);
517            walToHFiles(walBackupFileList, Arrays.asList(table.getNameAsString()),
518              previousBackupTs);
519            break;
520          }
521        }
522      }
523    } else {
524      // get incremental backup file list and prepare parameters for DistCp
525      List<String> incrBackupFileList = backupInfo.getIncrBackupFileList();
526      // Get list of tables in incremental backup set
527      Set<TableName> tableSet = backupManager.getIncrementalBackupTableSet();
528      // filter missing files out (they have been copied by previous backups)
529      incrBackupFileList = filterMissingFiles(incrBackupFileList);
530      List<String> tableList = new ArrayList<String>();
531      for (TableName table : tableSet) {
532        // Check if table exists
533        if (tableExists(table, conn)) {
534          tableList.add(table.getNameAsString());
535        } else {
536          LOG.warn("Table " + table + " does not exists. Skipping in WAL converter");
537        }
538      }
539      walToHFiles(incrBackupFileList, tableList, previousBackupTs);
540    }
541  }
542
543  protected boolean tableExists(TableName table, Connection conn) throws IOException {
544    try (Admin admin = conn.getAdmin()) {
545      return admin.tableExists(table);
546    }
547  }
548
549  protected void walToHFiles(List<String> dirPaths, List<String> tableList, long previousBackupTs)
550    throws IOException {
551    Tool player = new WALPlayer();
552    Configuration conf = new Configuration(this.conf);
553
554    // Player reads all files in arbitrary directory structure and creates
555    // a Map task for each file. We use ';' as separator
556    // because WAL file names contains ','
557    String dirs = StringUtils.join(dirPaths, ';');
558    String jobname = "Incremental_Backup-" + backupId + "-" + System.currentTimeMillis();
559
560    setBulkOutputPath(conf, tableList);
561    conf.set(WALPlayer.INPUT_FILES_SEPARATOR_KEY, ";");
562    conf.setBoolean(WALPlayer.MULTI_TABLES_SUPPORT, true);
563    conf.set(JOB_NAME_CONF_KEY, jobname);
564    conf.setBoolean(HFileOutputFormat2.DISK_BASED_SORTING_ENABLED_KEY, true);
565    if (backupInfo.isContinuousBackupEnabled()) {
566      conf.set(WALInputFormat.START_TIME_KEY, Long.toString(previousBackupTs));
567      conf.set(WALInputFormat.END_TIME_KEY, Long.toString(backupInfo.getIncrCommittedWalTs()));
568      // We do not want a multi-table HFile format here because continuous backups run the WALPlayer
569      // individually on each table in the backup set.
570      conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false);
571    }
572    String[] playerArgs = { dirs, StringUtils.join(tableList, ",") };
573
574    try {
575      player.setConf(conf);
576      int result = player.run(playerArgs);
577      if (result != 0) {
578        throw new IOException("WAL Player failed");
579      }
580    } catch (IOException e) {
581      throw e;
582    } catch (Exception ee) {
583      throw new IOException("Can not convert from directory " + dirs
584        + " (check Hadoop, HBase and WALPlayer M/R job logs) ", ee);
585    }
586  }
587
588  private void setBulkOutputPath(Configuration conf, List<String> tableList) {
589    Path bulkOutputPath = getBulkOutputDir();
590    if (backupInfo.isContinuousBackupEnabled()) {
591      if (tableList.size() != 1) {
592        // Continuous backups run the WALPlayer job on one table at a time, so the list of tables
593        // should have only one element.
594        throw new RuntimeException(
595          "Expected table list to have only one element, but got: " + tableList);
596      }
597      bulkOutputPath = getTmpBackupDirForTable(TableName.valueOf(tableList.get(0)));
598    }
599    conf.set(WALPlayer.BULK_OUTPUT_CONF_KEY, bulkOutputPath.toString());
600  }
601
602  private void incrementalCopyBulkloadHFiles(FileSystem tgtFs, TableName tn) throws IOException {
603    Path bulkOutDir = getBulkOutputDirForTable(tn);
604
605    if (tgtFs.exists(bulkOutDir)) {
606      conf.setInt(NUMBER_OF_LEVELS_TO_PRESERVE_KEY, 2);
607      LOG.debug(
608        "{} has been set to {}. This affects what source files are actually copied in the "
609          + "next Incremental copy HFiles job",
610        NUMBER_OF_LEVELS_TO_PRESERVE_KEY, conf.get(NUMBER_OF_LEVELS_TO_PRESERVE_KEY));
611      Path tgtPath = getTargetDirForTable(tn);
612      try {
613        RemoteIterator<LocatedFileStatus> locatedFiles = tgtFs.listFiles(bulkOutDir, true);
614        List<String> files = new ArrayList<>();
615        while (locatedFiles.hasNext()) {
616          LocatedFileStatus file = locatedFiles.next();
617          if (file.isFile() && HFile.isHFileFormat(tgtFs, file.getPath())) {
618            files.add(file.getPath().toString());
619          }
620        }
621        incrementalCopyHFiles(files.toArray(files.toArray(new String[0])), tgtPath.toString());
622      } finally {
623        conf.unset(NUMBER_OF_LEVELS_TO_PRESERVE_KEY);
624        LOG.debug("{} has been unset", NUMBER_OF_LEVELS_TO_PRESERVE_KEY);
625      }
626    }
627  }
628
629  /**
630   * Creates a path to the bulk load output directory for a table. This directory will look like:
631   * .../backupRoot/.tmp/backupId/namespace/table/data
632   * @param table The table whose HFiles are being bulk loaded
633   * @return A Path object representing the directory
634   */
635  protected Path getBulkOutputDirForTable(TableName table) {
636    Path tablePath = getTmpBackupDirForTable(table);
637    return new Path(tablePath, "data");
638  }
639
640  /**
641   * Creates a path to a table's directory within the temporary directory. This directory will look
642   * like: .../backupRoot/.tmp/backupId/namespace/table
643   * @param table The table whose HFiles are being bulk loaded
644   * @return A Path object representing the directory
645   */
646  protected Path getTmpBackupDirForTable(TableName table) {
647    Path tablePath = getBulkOutputDir();
648    tablePath = new Path(tablePath, table.getNamespaceAsString());
649    return new Path(tablePath, table.getQualifierAsString());
650  }
651
652  /**
653   * Creates a path to a temporary backup directory. This directory will look like:
654   * .../backupRoot/.tmp/backupId
655   * @return A Path object representing the directory
656   */
657  protected Path getBulkOutputDir() {
658    return new Path(getTmpBackupDir(), backupId);
659  }
660
661  /**
662   * Creates a path to the backup root directory's temporary subdirectory. The directory will look
663   * like: .../backupRoot/.tmp
664   * @return A Path object representing the directory
665   */
666  protected Path getTmpBackupDir() {
667    return new Path(backupInfo.getBackupRootDir(), ".tmp");
668  }
669
670  /**
671   * Creates a path to a destination directory for bulk loaded HFiles. This directory will look
672   * like: .../backupRoot/backupID/namespace/table
673   * @param table The table whose HFiles are being bulk loaded
674   * @return A Path object representing the directory
675   */
676  private Path getTargetDirForTable(TableName table) {
677    Path path = new Path(backupInfo.getBackupRootDir() + Path.SEPARATOR + backupInfo.getBackupId());
678    path = new Path(path, table.getNamespaceAsString());
679    path = new Path(path, table.getQualifierAsString());
680    return path;
681  }
682
683  private void setupRegionLocator() throws IOException {
684    Map<TableName, String> fullBackupIds = getFullBackupIds();
685    try (BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
686
687      for (TableName tableName : backupInfo.getTables()) {
688        String fullBackupId = fullBackupIds.get(tableName);
689        BackupInfo fullBackupInfo = backupAdmin.getBackupInfo(fullBackupId);
690        String snapshotName = fullBackupInfo.getSnapshotName(tableName);
691        Path root = HBackupFileSystem.getTableBackupPath(tableName,
692          new Path(fullBackupInfo.getBackupRootDir()), fullBackupId);
693        String manifestDir =
694          SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, root).toString();
695        SnapshotRegionLocator.setSnapshotManifestDir(conf, manifestDir, tableName);
696      }
697    }
698  }
699
700  private Map<TableName, String> getFullBackupIds() throws IOException {
701    // Ancestors are stored from newest to oldest, so we can iterate backwards
702    // in order to populate our backupId map with the most recent full backup
703    // for a given table
704    List<BackupManifest.BackupImage> images = getAncestors(backupInfo);
705    Map<TableName, String> results = new HashMap<>();
706    for (int i = images.size() - 1; i >= 0; i--) {
707      BackupManifest.BackupImage image = images.get(i);
708      if (image.getType() != BackupType.FULL) {
709        continue;
710      }
711
712      for (TableName tn : image.getTableNames()) {
713        results.put(tn, image.getBackupId());
714      }
715    }
716    return results;
717  }
718
719  /**
720   * Verifies that the current table descriptor CFs matches the descriptor CFs of the last full
721   * backup for the tables. This ensures CF compatibility across incremental backups. If a mismatch
722   * is detected, a full table backup should be taken, rather than an incremental one
723   */
724  private void verifyCfCompatibility(Set<TableName> tables,
725    Map<TableName, String> tablesToFullBackupId) throws IOException, ColumnFamilyMismatchException {
726    ColumnFamilyMismatchException.ColumnFamilyMismatchExceptionBuilder exBuilder =
727      ColumnFamilyMismatchException.newBuilder();
728    try (Admin admin = conn.getAdmin(); BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
729      for (TableName tn : tables) {
730        String backupId = tablesToFullBackupId.get(tn);
731        BackupInfo fullBackupInfo = backupAdmin.getBackupInfo(backupId);
732
733        ColumnFamilyDescriptor[] currentCfs = admin.getDescriptor(tn).getColumnFamilies();
734        String snapshotName = fullBackupInfo.getSnapshotName(tn);
735        Path root = HBackupFileSystem.getTableBackupPath(tn,
736          new Path(fullBackupInfo.getBackupRootDir()), fullBackupInfo.getBackupId());
737        Path manifestDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, root);
738
739        FileSystem fs;
740        try {
741          fs = FileSystem.get(new URI(fullBackupInfo.getBackupRootDir()), conf);
742        } catch (URISyntaxException e) {
743          throw new IOException("Unable to get fs for backup " + fullBackupInfo.getBackupId(), e);
744        }
745
746        SnapshotProtos.SnapshotDescription snapshotDescription =
747          SnapshotDescriptionUtils.readSnapshotInfo(fs, manifestDir);
748        SnapshotManifest manifest =
749          SnapshotManifest.open(conf, fs, manifestDir, snapshotDescription);
750        if (
751          SnapshotDescriptionUtils.isExpiredSnapshot(snapshotDescription.getTtl(),
752            snapshotDescription.getCreationTime(), EnvironmentEdgeManager.currentTime())
753        ) {
754          throw new SnapshotTTLExpiredException(
755            ProtobufUtil.createSnapshotDesc(snapshotDescription));
756        }
757
758        ColumnFamilyDescriptor[] backupCfs = manifest.getTableDescriptor().getColumnFamilies();
759        if (!areCfsCompatible(currentCfs, backupCfs)) {
760          exBuilder.addMismatchedTable(tn, currentCfs, backupCfs);
761        }
762      }
763    }
764
765    ColumnFamilyMismatchException ex = exBuilder.build();
766    if (!ex.getMismatchedTables().isEmpty()) {
767      throw ex;
768    }
769  }
770
771  private static boolean areCfsCompatible(ColumnFamilyDescriptor[] currentCfs,
772    ColumnFamilyDescriptor[] backupCfs) {
773    if (currentCfs.length != backupCfs.length) {
774      return false;
775    }
776
777    for (int i = 0; i < backupCfs.length; i++) {
778      String currentCf = currentCfs[i].getNameAsString();
779      String backupCf = backupCfs[i].getNameAsString();
780
781      if (!currentCf.equals(backupCf)) {
782        return false;
783      }
784    }
785
786    return true;
787  }
788}