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.withRoot;
021import static org.apache.hadoop.hbase.backup.BackupInfo.withState;
022import static org.apache.hadoop.hbase.backup.BackupInfo.withType;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.fs.FileSystem;
035import org.apache.hadoop.fs.Path;
036import org.apache.hadoop.hbase.TableName;
037import org.apache.hadoop.hbase.backup.BackupAdmin;
038import org.apache.hadoop.hbase.backup.BackupClientFactory;
039import org.apache.hadoop.hbase.backup.BackupInfo;
040import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
041import org.apache.hadoop.hbase.backup.BackupMergeJob;
042import org.apache.hadoop.hbase.backup.BackupRequest;
043import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
044import org.apache.hadoop.hbase.backup.BackupRestoreFactory;
045import org.apache.hadoop.hbase.backup.BackupType;
046import org.apache.hadoop.hbase.backup.HBackupFileSystem;
047import org.apache.hadoop.hbase.backup.RestoreRequest;
048import org.apache.hadoop.hbase.backup.util.BackupSet;
049import org.apache.hadoop.hbase.backup.util.BackupUtils;
050import org.apache.hadoop.hbase.client.Admin;
051import org.apache.hadoop.hbase.client.Connection;
052import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
053import org.apache.yetus.audience.InterfaceAudience;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
058
059@InterfaceAudience.Private
060public class BackupAdminImpl implements BackupAdmin {
061  public final static String CHECK_OK = "Checking backup images: OK";
062  public final static String CHECK_FAILED =
063    "Checking backup images: Failed. Some dependencies are missing for restore";
064  private static final Logger LOG = LoggerFactory.getLogger(BackupAdminImpl.class);
065
066  private final Connection conn;
067
068  public BackupAdminImpl(Connection conn) {
069    this.conn = conn;
070  }
071
072  @Override
073  public void close() {
074  }
075
076  @Override
077  public BackupInfo getBackupInfo(String backupId) throws IOException {
078    BackupInfo backupInfo;
079    try (final BackupSystemTable table = new BackupSystemTable(conn)) {
080      if (backupId == null) {
081        List<BackupInfo> recentSessions = table.getBackupInfos(withState(BackupState.RUNNING));
082        if (recentSessions.isEmpty()) {
083          LOG.warn("No ongoing sessions found.");
084          return null;
085        }
086        // else show status for ongoing session
087        // must be one maximum
088        return recentSessions.get(0);
089      } else {
090        backupInfo = table.readBackupInfo(backupId);
091        return backupInfo;
092      }
093    }
094  }
095
096  @Override
097  public int deleteBackups(String[] backupIds) throws IOException {
098
099    int totalDeleted = 0;
100
101    boolean deleteSessionStarted;
102    boolean snapshotDone;
103    try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
104      // Step 1: Make sure there is no active session
105      // is running by using startBackupSession API
106      // If there is an active session in progress, exception will be thrown
107      try {
108        sysTable.startBackupExclusiveOperation();
109        deleteSessionStarted = true;
110      } catch (IOException e) {
111        LOG.warn("You can not run delete command while active backup session is in progress. \n"
112          + "If there is no active backup session running, run backup repair utility to "
113          + "restore \nbackup system integrity.");
114        return -1;
115      }
116
117      // Step 2: Make sure there is no failed session
118      List<BackupInfo> list = sysTable.getBackupInfos(withState(BackupState.RUNNING));
119      if (list.size() != 0) {
120        // ailed sessions found
121        LOG.warn("Failed backup session found. Run backup repair tool first.");
122        return -1;
123      }
124
125      // Step 3: Record delete session
126      sysTable.startDeleteOperation(backupIds);
127      // Step 4: Snapshot backup system table
128      if (!BackupSystemTable.snapshotExists(conn)) {
129        BackupSystemTable.snapshot(conn);
130      } else {
131        LOG.warn("Backup system table snapshot exists");
132      }
133      snapshotDone = true;
134      try {
135        List<String> affectedBackupRootDirs = new ArrayList<>();
136        for (int i = 0; i < backupIds.length; i++) {
137          BackupInfo info = sysTable.readBackupInfo(backupIds[i]);
138          if (info == null) {
139            continue;
140          }
141          affectedBackupRootDirs.add(info.getBackupRootDir());
142          totalDeleted += deleteBackup(backupIds[i], sysTable);
143        }
144        finalizeDelete(affectedBackupRootDirs, sysTable);
145        // Finish
146        sysTable.finishDeleteOperation();
147        // delete snapshot
148        BackupSystemTable.deleteSnapshot(conn);
149      } catch (IOException e) {
150        // Fail delete operation
151        // Step 1
152        if (snapshotDone) {
153          if (BackupSystemTable.snapshotExists(conn)) {
154            BackupSystemTable.restoreFromSnapshot(conn);
155            // delete snapshot
156            BackupSystemTable.deleteSnapshot(conn);
157            // We still have record with unfinished delete operation
158            LOG.error("Delete operation failed, please run backup repair utility to restore "
159              + "backup system integrity", e);
160            throw e;
161          } else {
162            LOG.warn("Delete operation succeeded, there were some errors: ", e);
163          }
164        }
165
166      } finally {
167        if (deleteSessionStarted) {
168          sysTable.finishBackupExclusiveOperation();
169        }
170      }
171    }
172    return totalDeleted;
173  }
174
175  /**
176   * Updates incremental backup set for every backupRoot
177   * @param backupRoots backupRoots for which to revise the incremental backup set
178   * @param table       backup system table
179   * @throws IOException if a table operation fails
180   */
181  private void finalizeDelete(List<String> backupRoots, BackupSystemTable table)
182    throws IOException {
183    for (String backupRoot : backupRoots) {
184      Set<TableName> incrTableSet = table.getIncrementalBackupTableSet(backupRoot);
185      Map<TableName, List<BackupInfo>> tableMap =
186        table.getBackupHistoryForTableSet(incrTableSet, backupRoot);
187
188      // Keep only the tables that are present in other backups
189      incrTableSet.retainAll(tableMap.keySet());
190
191      table.deleteIncrementalBackupTableSet(backupRoot);
192      if (!incrTableSet.isEmpty()) {
193        table.addIncrementalBackupTableSet(incrTableSet, backupRoot);
194      }
195    }
196  }
197
198  /**
199   * Delete single backup and all related backups <br>
200   * Algorithm:<br>
201   * Backup type: FULL or INCREMENTAL <br>
202   * Is this last backup session for table T: YES or NO <br>
203   * For every table T from table list 'tables':<br>
204   * if(FULL, YES) deletes only physical data (PD) <br>
205   * if(FULL, NO), deletes PD, scans all newer backups and removes T from backupInfo,<br>
206   * until we either reach the most recent backup for T in the system or FULL backup<br>
207   * which includes T<br>
208   * if(INCREMENTAL, YES) deletes only physical data (PD) if(INCREMENTAL, NO) deletes physical data
209   * and for table T scans all backup images between last<br>
210   * FULL backup, which is older than the backup being deleted and the next FULL backup (if exists)
211   * <br>
212   * or last one for a particular table T and removes T from list of backup tables.
213   * @param backupId backup id
214   * @param sysTable backup system table
215   * @return total number of deleted backup images
216   * @throws IOException if deleting the backup fails
217   */
218  private int deleteBackup(String backupId, BackupSystemTable sysTable) throws IOException {
219    BackupInfo backupInfo = sysTable.readBackupInfo(backupId);
220
221    int totalDeleted = 0;
222    if (backupInfo != null) {
223      LOG.info("Deleting backup " + backupInfo.getBackupId() + " ...");
224      // Step 1: clean up data for backup session (idempotent)
225      BackupUtils.cleanupBackupData(backupInfo, conn.getConfiguration());
226      // List of tables in this backup;
227      List<TableName> tables = backupInfo.getTableNames();
228      long startTime = backupInfo.getStartTs();
229      for (TableName tn : tables) {
230        boolean isLastBackupSession = isLastBackupSession(sysTable, tn, startTime);
231        if (isLastBackupSession) {
232          continue;
233        }
234        // else
235        List<BackupInfo> affectedBackups = getAffectedBackupSessions(backupInfo, tn, sysTable);
236        for (BackupInfo info : affectedBackups) {
237          if (info.equals(backupInfo)) {
238            continue;
239          }
240          removeTableFromBackupImage(info, tn, sysTable);
241        }
242      }
243      Map<byte[], String> map = sysTable.readBulkLoadedFiles(backupId);
244      FileSystem fs = FileSystem.get(conn.getConfiguration());
245      boolean success = true;
246      int numDeleted = 0;
247      for (String f : map.values()) {
248        Path p = new Path(f);
249        try {
250          LOG.debug("Delete backup info " + p + " for " + backupInfo.getBackupId());
251          if (!fs.delete(p)) {
252            if (fs.exists(p)) {
253              LOG.warn(f + " was not deleted");
254              success = false;
255            }
256          } else {
257            numDeleted++;
258          }
259        } catch (IOException ioe) {
260          LOG.warn(f + " was not deleted", ioe);
261          success = false;
262        }
263      }
264      if (LOG.isDebugEnabled()) {
265        LOG.debug(numDeleted + " bulk loaded files out of " + map.size() + " were deleted");
266      }
267      if (success) {
268        sysTable.deleteBulkLoadedRows(new ArrayList<>(map.keySet()));
269      }
270
271      sysTable.deleteBackupInfo(backupInfo.getBackupId());
272      LOG.info("Delete backup " + backupInfo.getBackupId() + " completed.");
273      totalDeleted++;
274    } else {
275      LOG.warn("Delete backup failed: no information found for backupID=" + backupId);
276    }
277    return totalDeleted;
278  }
279
280  private void removeTableFromBackupImage(BackupInfo info, TableName tn, BackupSystemTable sysTable)
281    throws IOException {
282    List<TableName> tables = info.getTableNames();
283    LOG.debug(
284      "Remove " + tn + " from " + info.getBackupId() + " tables=" + info.getTableListAsString());
285    if (tables.contains(tn)) {
286      tables.remove(tn);
287
288      if (tables.isEmpty()) {
289        LOG.debug("Delete backup info " + info.getBackupId());
290
291        sysTable.deleteBackupInfo(info.getBackupId());
292        // Idempotent operation
293        BackupUtils.cleanupBackupData(info, conn.getConfiguration());
294      } else {
295        info.setTables(tables);
296        sysTable.updateBackupInfo(info);
297        // Now, clean up directory for table (idempotent)
298        cleanupBackupDir(info, tn, conn.getConfiguration());
299      }
300    }
301  }
302
303  private List<BackupInfo> getAffectedBackupSessions(BackupInfo backupInfo, TableName tn,
304    BackupSystemTable table) throws IOException {
305    LOG.debug("GetAffectedBackupInfos for: " + backupInfo.getBackupId() + " table=" + tn);
306    long ts = backupInfo.getStartTs();
307    List<BackupInfo> list = new ArrayList<>();
308    List<BackupInfo> history = table.getBackupHistory(withRoot(backupInfo.getBackupRootDir()));
309    // Scan from most recent to backupInfo
310    // break when backupInfo reached
311    for (BackupInfo info : history) {
312      if (info.getStartTs() == ts) {
313        break;
314      }
315      List<TableName> tables = info.getTableNames();
316      if (tables.contains(tn)) {
317        BackupType bt = info.getType();
318        if (bt == BackupType.FULL) {
319          // Clear list if we encounter FULL backup
320          list.clear();
321        } else {
322          LOG.debug("GetAffectedBackupInfos for: " + backupInfo.getBackupId() + " table=" + tn
323            + " added " + info.getBackupId() + " tables=" + info.getTableListAsString());
324          list.add(info);
325        }
326      }
327    }
328    return list;
329  }
330
331  /**
332   * Clean up the data at target directory
333   * @throws IOException if cleaning up the backup directory fails
334   */
335  private void cleanupBackupDir(BackupInfo backupInfo, TableName table, Configuration conf)
336    throws IOException {
337    try {
338      // clean up the data at target directory
339      String targetDir = backupInfo.getBackupRootDir();
340      if (targetDir == null) {
341        LOG.warn("No target directory specified for " + backupInfo.getBackupId());
342        return;
343      }
344
345      FileSystem outputFs = FileSystem.get(new Path(backupInfo.getBackupRootDir()).toUri(), conf);
346
347      Path targetDirPath = new Path(BackupUtils.getTableBackupDir(backupInfo.getBackupRootDir(),
348        backupInfo.getBackupId(), table));
349      if (outputFs.delete(targetDirPath, true)) {
350        LOG.info("Cleaning up backup data at " + targetDirPath.toString() + " done.");
351      } else {
352        LOG.info("No data has been found in " + targetDirPath.toString() + ".");
353      }
354    } catch (IOException e1) {
355      LOG.error("Cleaning up backup data of " + backupInfo.getBackupId() + " for table " + table
356        + "at " + backupInfo.getBackupRootDir() + " failed due to " + e1.getMessage() + ".");
357      throw e1;
358    }
359  }
360
361  private boolean isLastBackupSession(BackupSystemTable table, TableName tn, long startTime)
362    throws IOException {
363    List<BackupInfo> history = table.getBackupHistory();
364    for (BackupInfo info : history) {
365      List<TableName> tables = info.getTableNames();
366      if (!tables.contains(tn)) {
367        continue;
368      }
369      return info.getStartTs() <= startTime;
370    }
371    return false;
372  }
373
374  @Override
375  public List<BackupInfo> getHistory(int n, BackupInfo.Filter... filters) throws IOException {
376    try (final BackupSystemTable table = new BackupSystemTable(conn)) {
377      return table.getBackupInfos(n, filters);
378    }
379  }
380
381  @Override
382  public List<BackupSet> listBackupSets() throws IOException {
383    try (final BackupSystemTable table = new BackupSystemTable(conn)) {
384      List<String> list = table.listBackupSets();
385      List<BackupSet> bslist = new ArrayList<>();
386      for (String s : list) {
387        List<TableName> tables = table.describeBackupSet(s);
388        if (tables != null) {
389          bslist.add(new BackupSet(s, tables));
390        }
391      }
392      return bslist;
393    }
394  }
395
396  @Override
397  public BackupSet getBackupSet(String name) throws IOException {
398    try (final BackupSystemTable table = new BackupSystemTable(conn)) {
399      List<TableName> list = table.describeBackupSet(name);
400
401      if (list == null) {
402        return null;
403      }
404
405      return new BackupSet(name, list);
406    }
407  }
408
409  @Override
410  public boolean deleteBackupSet(String name) throws IOException {
411    try (final BackupSystemTable table = new BackupSystemTable(conn)) {
412      if (table.describeBackupSet(name) == null) {
413        return false;
414      }
415      table.deleteBackupSet(name);
416      return true;
417    }
418  }
419
420  @Override
421  public void addToBackupSet(String name, TableName[] tables) throws IOException {
422    String[] tableNames = new String[tables.length];
423    try (final BackupSystemTable table = new BackupSystemTable(conn);
424      final Admin admin = conn.getAdmin()) {
425      for (int i = 0; i < tables.length; i++) {
426        tableNames[i] = tables[i].getNameAsString();
427        if (!admin.tableExists(TableName.valueOf(tableNames[i]))) {
428          throw new IOException("Cannot add " + tableNames[i] + " because it doesn't exist");
429        }
430      }
431      table.addToBackupSet(name, tableNames);
432      LOG.info(
433        "Added tables [" + StringUtils.join(tableNames, " ") + "] to '" + name + "' backup set");
434    }
435  }
436
437  @Override
438  public void removeFromBackupSet(String name, TableName[] tables) throws IOException {
439    LOG.info("Removing tables [" + StringUtils.join(tables, " ") + "] from '" + name + "'");
440    try (final BackupSystemTable table = new BackupSystemTable(conn)) {
441      table.removeFromBackupSet(name, toStringArray(tables));
442      LOG.info(
443        "Removing tables [" + StringUtils.join(tables, " ") + "] from '" + name + "' completed.");
444    }
445  }
446
447  private String[] toStringArray(TableName[] list) {
448    String[] arr = new String[list.length];
449    for (int i = 0; i < list.length; i++) {
450      arr[i] = list[i].toString();
451    }
452    return arr;
453  }
454
455  @Override
456  public void restore(RestoreRequest request) throws IOException {
457    if (request.isCheck()) {
458      // check and load backup image manifest for the tables
459      Path rootPath = new Path(request.getBackupRootDir());
460      String backupId = request.getBackupId();
461      TableName[] sTableArray = request.getFromTables();
462      BackupManifest manifest =
463        HBackupFileSystem.getManifest(conn.getConfiguration(), rootPath, backupId);
464
465      // Check and validate the backup image and its dependencies
466      if (BackupUtils.validate(Arrays.asList(sTableArray), manifest, conn.getConfiguration())) {
467        LOG.info(CHECK_OK);
468      } else {
469        LOG.error(CHECK_FAILED);
470      }
471      return;
472    }
473    // Execute restore request
474    new RestoreTablesClient(conn, request).execute();
475  }
476
477  @Override
478  public String backupTables(BackupRequest request) throws IOException {
479    BackupType type = request.getBackupType();
480    String targetRootDir = request.getTargetRootDir();
481    List<TableName> tableList = request.getTableList();
482
483    String backupId = BackupRestoreConstants.BACKUPID_PREFIX + EnvironmentEdgeManager.currentTime();
484    if (type == BackupType.INCREMENTAL) {
485      Set<TableName> incrTableSet;
486      try (BackupSystemTable table = new BackupSystemTable(conn)) {
487        incrTableSet = table.getIncrementalBackupTableSet(targetRootDir);
488      }
489
490      if (incrTableSet.isEmpty()) {
491        String msg =
492          "Incremental backup table set contains no tables. " + "You need to run full backup first "
493            + (tableList != null ? "on " + StringUtils.join(tableList, ",") : "");
494
495        throw new IOException(msg);
496      }
497      if (tableList != null) {
498        tableList.removeAll(incrTableSet);
499        if (!tableList.isEmpty()) {
500          String extraTables = StringUtils.join(tableList, ",");
501          String msg = "Some tables (" + extraTables + ") haven't gone through full backup. "
502            + "Perform full backup on " + extraTables + " first, " + "then retry the command";
503          throw new IOException(msg);
504        }
505      }
506      tableList = Lists.newArrayList(incrTableSet);
507    }
508    if (tableList != null && !tableList.isEmpty()) {
509      for (TableName table : tableList) {
510        String targetTableBackupDir =
511          HBackupFileSystem.getTableBackupDir(targetRootDir, backupId, table);
512        Path targetTableBackupDirPath = new Path(targetTableBackupDir);
513        FileSystem outputFs =
514          FileSystem.get(targetTableBackupDirPath.toUri(), conn.getConfiguration());
515        if (outputFs.exists(targetTableBackupDirPath)) {
516          throw new IOException(
517            "Target backup directory " + targetTableBackupDir + " exists already.");
518        }
519        outputFs.mkdirs(targetTableBackupDirPath);
520      }
521      ArrayList<TableName> nonExistingTableList = null;
522      try (Admin admin = conn.getAdmin()) {
523        for (TableName tableName : tableList) {
524          if (!admin.tableExists(tableName)) {
525            if (nonExistingTableList == null) {
526              nonExistingTableList = new ArrayList<>();
527            }
528            nonExistingTableList.add(tableName);
529          }
530        }
531      }
532      if (nonExistingTableList != null) {
533        if (type == BackupType.INCREMENTAL) {
534          // Update incremental backup set
535          tableList = excludeNonExistingTables(tableList, nonExistingTableList);
536        } else {
537          // Throw exception only in full mode - we try to backup non-existing table
538          throw new IOException(
539            "Non-existing tables found in the table list: " + nonExistingTableList);
540        }
541      }
542    }
543
544    // update table list
545    BackupRequest.Builder builder = new BackupRequest.Builder();
546    request = builder.withBackupType(request.getBackupType()).withTableList(tableList)
547      .withTargetRootDir(request.getTargetRootDir()).withBackupSetName(request.getBackupSetName())
548      .withTotalTasks(request.getTotalTasks()).withBandwidthPerTasks((int) request.getBandwidth())
549      .withNoChecksumVerify(request.getNoChecksumVerify()).build();
550
551    TableBackupClient client;
552    try {
553      client = BackupClientFactory.create(conn, backupId, request);
554    } catch (IOException e) {
555      LOG.error("There is an active session already running");
556      throw e;
557    }
558
559    client.execute();
560
561    return backupId;
562  }
563
564  private List<TableName> excludeNonExistingTables(List<TableName> tableList,
565    List<TableName> nonExistingTableList) {
566    for (TableName table : nonExistingTableList) {
567      tableList.remove(table);
568    }
569    return tableList;
570  }
571
572  @Override
573  public void mergeBackups(String[] backupIds) throws IOException {
574    try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
575      checkIfValidForMerge(backupIds, sysTable);
576      // TODO run job on remote cluster
577      BackupMergeJob job = BackupRestoreFactory.getBackupMergeJob(conn.getConfiguration());
578      job.run(backupIds);
579    }
580  }
581
582  /**
583   * Verifies that backup images are valid for merge.
584   * <ul>
585   * <li>All backups MUST be in the same destination
586   * <li>No FULL backups are allowed - only INCREMENTAL
587   * <li>All backups must be in COMPLETE state
588   * <li>No holes in backup list are allowed
589   * </ul>
590   * <p>
591   * @param backupIds list of backup ids
592   * @param table     backup system table
593   * @throws IOException if the backup image is not valid for merge
594   */
595  private void checkIfValidForMerge(String[] backupIds, BackupSystemTable table)
596    throws IOException {
597    String backupRoot = null;
598
599    final Set<TableName> allTables = new HashSet<>();
600    final Set<String> allBackups = new HashSet<>();
601    long minTime = Long.MAX_VALUE, maxTime = Long.MIN_VALUE;
602    for (String backupId : backupIds) {
603      BackupInfo bInfo = table.readBackupInfo(backupId);
604      if (bInfo == null) {
605        String msg = "Backup session " + backupId + " not found";
606        throw new IOException(msg);
607      }
608      if (backupRoot == null) {
609        backupRoot = bInfo.getBackupRootDir();
610      } else if (!bInfo.getBackupRootDir().equals(backupRoot)) {
611        throw new IOException("Found different backup destinations in a list of a backup sessions "
612          + "\n1. " + backupRoot + "\n" + "2. " + bInfo.getBackupRootDir());
613      }
614      if (bInfo.getType() == BackupType.FULL) {
615        throw new IOException("FULL backup image can not be merged for: \n" + bInfo);
616      }
617
618      if (bInfo.getState() != BackupState.COMPLETE) {
619        throw new IOException("Backup image " + backupId
620          + " can not be merged becuase of its state: " + bInfo.getState());
621      }
622      allBackups.add(backupId);
623      allTables.addAll(bInfo.getTableNames());
624      long time = bInfo.getStartTs();
625      if (time < minTime) {
626        minTime = time;
627      }
628      if (time > maxTime) {
629        maxTime = time;
630      }
631    }
632
633    final long startRangeTime = minTime;
634    final long endRangeTime = maxTime;
635    final String backupDest = backupRoot;
636    // Check we have no 'holes' in backup id list
637    // Filter 1 : backupRoot
638    // Filter 2 : time range filter
639    // Filter 3 : table filter
640    BackupInfo.Filter destinationFilter = withRoot(backupDest);
641
642    BackupInfo.Filter timeRangeFilter = info -> {
643      long time = info.getStartTs();
644      return time >= startRangeTime && time <= endRangeTime;
645    };
646
647    BackupInfo.Filter tableFilter = info -> {
648      List<TableName> tables = info.getTableNames();
649      return !Collections.disjoint(allTables, tables);
650    };
651
652    BackupInfo.Filter typeFilter = withType(BackupType.INCREMENTAL);
653    BackupInfo.Filter stateFilter = withState(BackupState.COMPLETE);
654
655    List<BackupInfo> allInfos = table.getBackupHistory(destinationFilter, timeRangeFilter,
656      tableFilter, typeFilter, stateFilter);
657    if (allInfos.size() != allBackups.size()) {
658      // Yes we have at least one hole in backup image sequence
659      List<String> missingIds = new ArrayList<>();
660      for (BackupInfo info : allInfos) {
661        if (allBackups.contains(info.getBackupId())) {
662          continue;
663        }
664        missingIds.add(info.getBackupId());
665      }
666      String errMsg =
667        "Sequence of backup ids has 'holes'. The following backup images must be added:"
668          + org.apache.hadoop.util.StringUtils.join(",", missingIds);
669      throw new IOException(errMsg);
670    }
671  }
672}