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