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