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}