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}