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