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