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.replication; 019 020import static org.apache.hadoop.hbase.HConstants.REPLICATION_BULKLOAD_ENABLE_KEY; 021import static org.apache.hadoop.hbase.HConstants.REPLICATION_CLUSTER_ID; 022import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.CONF_BACKUP_ROOT_DIR; 023import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.CONF_PEER_UUID; 024import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.ONE_DAY_IN_MILLISECONDS; 025import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.WAL_FILE_PREFIX; 026import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.copyWithCleanup; 027import static org.apache.hadoop.hbase.backup.util.BackupFileSystemManager.BULKLOAD_FILES_DIR; 028import static org.apache.hadoop.hbase.backup.util.BackupFileSystemManager.WALS_DIR; 029import static org.apache.hadoop.hbase.backup.util.BackupUtils.DATE_FORMAT; 030import static org.junit.jupiter.api.Assertions.assertEquals; 031import static org.junit.jupiter.api.Assertions.assertFalse; 032import static org.junit.jupiter.api.Assertions.assertNotNull; 033import static org.junit.jupiter.api.Assertions.assertThrows; 034import static org.junit.jupiter.api.Assertions.assertTrue; 035import static org.junit.jupiter.api.Assertions.fail; 036import static org.mockito.ArgumentMatchers.eq; 037import static org.mockito.Mockito.mock; 038import static org.mockito.Mockito.mockStatic; 039import static org.mockito.Mockito.verify; 040import static org.mockito.Mockito.when; 041 042import java.io.IOException; 043import java.text.SimpleDateFormat; 044import java.util.ArrayList; 045import java.util.Date; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.List; 049import java.util.Map; 050import java.util.Set; 051import java.util.TimeZone; 052import java.util.UUID; 053import java.util.concurrent.atomic.AtomicBoolean; 054import org.apache.hadoop.conf.Configuration; 055import org.apache.hadoop.fs.FSDataOutputStream; 056import org.apache.hadoop.fs.FileStatus; 057import org.apache.hadoop.fs.FileSystem; 058import org.apache.hadoop.fs.FileUtil; 059import org.apache.hadoop.fs.LocatedFileStatus; 060import org.apache.hadoop.fs.Path; 061import org.apache.hadoop.fs.RemoteIterator; 062import org.apache.hadoop.hbase.HBaseTestingUtil; 063import org.apache.hadoop.hbase.TableName; 064import org.apache.hadoop.hbase.backup.util.BackupFileSystemManager; 065import org.apache.hadoop.hbase.backup.util.BackupUtils; 066import org.apache.hadoop.hbase.client.Admin; 067import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 068import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 069import org.apache.hadoop.hbase.client.Table; 070import org.apache.hadoop.hbase.client.TableDescriptor; 071import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 072import org.apache.hadoop.hbase.mapreduce.WALPlayer; 073import org.apache.hadoop.hbase.replication.ReplicationPeerConfig; 074import org.apache.hadoop.hbase.testclassification.LargeTests; 075import org.apache.hadoop.hbase.testclassification.ReplicationTests; 076import org.apache.hadoop.hbase.tool.BulkLoadHFiles; 077import org.apache.hadoop.hbase.tool.BulkLoadHFilesTool; 078import org.apache.hadoop.hbase.util.Bytes; 079import org.apache.hadoop.hbase.util.EnvironmentEdgeManagerTestHelper; 080import org.apache.hadoop.hbase.util.HFileTestUtil; 081import org.apache.hadoop.hbase.util.ManualEnvironmentEdge; 082import org.apache.hadoop.util.ToolRunner; 083import org.junit.jupiter.api.AfterAll; 084import org.junit.jupiter.api.BeforeAll; 085import org.junit.jupiter.api.Tag; 086import org.junit.jupiter.api.Test; 087import org.mockito.MockedStatic; 088import org.slf4j.Logger; 089import org.slf4j.LoggerFactory; 090 091@Tag(ReplicationTests.TAG) 092@Tag(LargeTests.TAG) 093public class TestContinuousBackupReplicationEndpoint { 094 095 private static final Logger LOG = 096 LoggerFactory.getLogger(TestContinuousBackupReplicationEndpoint.class); 097 098 private final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 099 private static final Configuration conf = TEST_UTIL.getConfiguration(); 100 private static Admin admin; 101 102 private final String replicationEndpoint = ContinuousBackupReplicationEndpoint.class.getName(); 103 private static final String CF_NAME = "cf"; 104 private static final byte[] QUALIFIER = Bytes.toBytes("my-qualifier"); 105 static FileSystem fs = null; 106 static Path root; 107 108 @BeforeAll 109 public static void setUpBeforeClass() throws Exception { 110 // Set the configuration properties as required 111 conf.setBoolean(REPLICATION_BULKLOAD_ENABLE_KEY, true); 112 conf.set(REPLICATION_CLUSTER_ID, "clusterId1"); 113 114 TEST_UTIL.startMiniZKCluster(); 115 TEST_UTIL.startMiniCluster(3); 116 fs = FileSystem.get(conf); 117 root = TEST_UTIL.getDataTestDirOnTestFS(); 118 admin = TEST_UTIL.getAdmin(); 119 } 120 121 @AfterAll 122 public static void tearDownAfterClass() throws Exception { 123 if (fs != null) { 124 fs.close(); 125 } 126 TEST_UTIL.shutdownMiniCluster(); 127 } 128 129 @Test 130 public void testWALAndBulkLoadFileBackup() throws IOException { 131 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 132 TableName tableName = TableName.valueOf("table_" + methodName); 133 String peerId = "peerId"; 134 135 createTable(tableName); 136 137 Path backupRootDir = new Path(root, methodName); 138 fs.mkdirs(backupRootDir); 139 140 Map<TableName, List<String>> tableMap = new HashMap<>(); 141 tableMap.put(tableName, new ArrayList<>()); 142 143 addReplicationPeer(peerId, backupRootDir, tableMap); 144 145 loadRandomData(tableName, 100); 146 assertEquals(100, getRowCount(tableName)); 147 148 Path dir = TEST_UTIL.getDataTestDirOnTestFS("testBulkLoadByFamily"); 149 generateHFiles(dir); 150 bulkLoadHFiles(tableName, dir); 151 assertEquals(1100, getRowCount(tableName)); 152 153 waitForReplication(15000); 154 deleteReplicationPeer(peerId); 155 156 verifyBackup(backupRootDir.toString(), true, Map.of(tableName, 1100)); 157 158 deleteTable(tableName); 159 } 160 161 @Test 162 public void testMultiTableWALBackup() throws IOException { 163 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 164 TableName table1 = TableName.valueOf("table_" + methodName + "1"); 165 TableName table2 = TableName.valueOf("table_" + methodName + "2"); 166 TableName table3 = TableName.valueOf("table_" + methodName + "3"); 167 String peerId = "peerMulti"; 168 169 for (TableName table : List.of(table1, table2, table3)) { 170 createTable(table); 171 } 172 173 Path backupRootDir = new Path(root, methodName); 174 fs.mkdirs(backupRootDir); 175 176 Map<TableName, List<String>> initialTableMap = new HashMap<>(); 177 initialTableMap.put(table1, new ArrayList<>()); 178 initialTableMap.put(table2, new ArrayList<>()); 179 180 addReplicationPeer(peerId, backupRootDir, initialTableMap); 181 182 for (TableName table : List.of(table1, table2, table3)) { 183 loadRandomData(table, 50); 184 assertEquals(50, getRowCount(table)); 185 } 186 187 waitForReplication(15000); 188 189 // Update the Replication Peer to Include table3 190 admin.updateReplicationPeerConfig(peerId, 191 ReplicationPeerConfig.newBuilder(admin.getReplicationPeerConfig(peerId)) 192 .setTableCFsMap( 193 Map.of(table1, new ArrayList<>(), table2, new ArrayList<>(), table3, new ArrayList<>())) 194 .build()); 195 196 for (TableName table : List.of(table1, table2, table3)) { 197 loadRandomData(table, 50); 198 assertEquals(100, getRowCount(table)); 199 } 200 201 waitForReplication(15000); 202 deleteReplicationPeer(peerId); 203 204 verifyBackup(backupRootDir.toString(), false, Map.of(table1, 100, table2, 100, table3, 50)); 205 206 for (TableName table : List.of(table1, table2, table3)) { 207 deleteTable(table); 208 } 209 } 210 211 @Test 212 public void testWALBackupWithPeerRestart() throws IOException, InterruptedException { 213 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 214 TableName tableName = TableName.valueOf("table_" + methodName); 215 String peerId = "peerId"; 216 217 createTable(tableName); 218 219 Path backupRootDir = new Path(root, methodName); 220 fs.mkdirs(backupRootDir); 221 222 Map<TableName, List<String>> tableMap = new HashMap<>(); 223 tableMap.put(tableName, new ArrayList<>()); 224 225 addReplicationPeer(peerId, backupRootDir, tableMap); 226 227 AtomicBoolean stopLoading = new AtomicBoolean(false); 228 229 // Start a separate thread to load data continuously 230 Thread dataLoaderThread = new Thread(() -> { 231 try { 232 while (!stopLoading.get()) { 233 loadRandomData(tableName, 10); 234 Thread.sleep(1000); // Simulate delay 235 } 236 } catch (Exception e) { 237 LOG.error("Data loading thread encountered an error", e); 238 } 239 }); 240 241 dataLoaderThread.start(); 242 243 // Main thread enables and disables replication peer 244 try { 245 for (int i = 0; i < 5; i++) { 246 LOG.info("Disabling replication peer..."); 247 admin.disableReplicationPeer(peerId); 248 Thread.sleep(2000); 249 250 LOG.info("Enabling replication peer..."); 251 admin.enableReplicationPeer(peerId); 252 Thread.sleep(2000); 253 } 254 } finally { 255 stopLoading.set(true); // Stop the data loader thread 256 dataLoaderThread.join(); 257 } 258 259 waitForReplication(20000); 260 deleteReplicationPeer(peerId); 261 262 verifyBackup(backupRootDir.toString(), false, Map.of(tableName, getRowCount(tableName))); 263 264 deleteTable(tableName); 265 } 266 267 @Test 268 public void testDayWiseWALBackup() throws IOException { 269 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 270 TableName tableName = TableName.valueOf("table_" + methodName); 271 String peerId = "peerId"; 272 273 createTable(tableName); 274 275 Path backupRootDir = new Path(root, methodName); 276 fs.mkdirs(backupRootDir); 277 278 Map<TableName, List<String>> tableMap = new HashMap<>(); 279 tableMap.put(tableName, new ArrayList<>()); 280 281 addReplicationPeer(peerId, backupRootDir, tableMap); 282 283 // Mock system time using ManualEnvironmentEdge 284 ManualEnvironmentEdge manualEdge = new ManualEnvironmentEdge(); 285 EnvironmentEdgeManagerTestHelper.injectEdge(manualEdge); 286 287 long currentTime = System.currentTimeMillis(); 288 long oneDayBackTime = currentTime - ONE_DAY_IN_MILLISECONDS; 289 290 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); 291 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 292 String expectedPrevDayDir = dateFormat.format(new Date(oneDayBackTime)); 293 String expectedCurrentDayDir = dateFormat.format(new Date(currentTime)); 294 295 manualEdge.setValue(oneDayBackTime); 296 loadRandomData(tableName, 100); 297 assertEquals(100, getRowCount(tableName)); 298 299 manualEdge.setValue(currentTime); 300 loadRandomData(tableName, 100); 301 assertEquals(200, getRowCount(tableName)); 302 303 // Reset time mocking 304 EnvironmentEdgeManagerTestHelper.reset(); 305 306 waitForReplication(15000); 307 deleteReplicationPeer(peerId); 308 309 verifyBackup(backupRootDir.toString(), false, Map.of(tableName, 200)); 310 311 // Verify that WALs are stored in two directories, one for each day 312 Path walDir = new Path(backupRootDir, WALS_DIR); 313 Set<String> walDirectories = new HashSet<>(); 314 315 FileStatus[] fileStatuses = fs.listStatus(walDir); 316 for (FileStatus fileStatus : fileStatuses) { 317 if (fileStatus.isDirectory()) { 318 String dirName = fileStatus.getPath().getName(); 319 walDirectories.add(dirName); 320 } 321 } 322 323 assertEquals(2, walDirectories.size(), "WALs should be stored in exactly two directories"); 324 assertTrue(walDirectories.contains(expectedPrevDayDir), 325 "Expected previous day's WAL directory missing"); 326 assertTrue(walDirectories.contains(expectedCurrentDayDir), 327 "Expected current day's WAL directory missing"); 328 329 deleteTable(tableName); 330 } 331 332 /** 333 * Simulates a one-time failure during bulk load file upload. This validates that the retry logic 334 * in the replication endpoint works as expected. 335 */ 336 @Test 337 public void testBulkLoadFileUploadRetry() throws IOException { 338 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 339 TableName tableName = TableName.valueOf("table_" + methodName); 340 String peerId = "peerId"; 341 342 // Reset static failure flag before test 343 FailingOnceContinuousBackupReplicationEndpoint.reset(); 344 345 createTable(tableName); 346 347 Path backupRootDir = new Path(root, methodName); 348 fs.mkdirs(backupRootDir); 349 350 Map<TableName, List<String>> tableMap = new HashMap<>(); 351 tableMap.put(tableName, new ArrayList<>()); 352 353 addReplicationPeer(peerId, backupRootDir, tableMap, 354 FailingOnceContinuousBackupReplicationEndpoint.class.getName()); 355 356 loadRandomData(tableName, 100); 357 assertEquals(100, getRowCount(tableName)); 358 359 Path dir = TEST_UTIL.getDataTestDirOnTestFS("testBulkLoadByFamily"); 360 generateHFiles(dir); 361 bulkLoadHFiles(tableName, dir); 362 assertEquals(1100, getRowCount(tableName)); 363 364 // Replication: first attempt fails, second attempt succeeds 365 waitForReplication(15000); 366 deleteReplicationPeer(peerId); 367 368 verifyBackup(backupRootDir.toString(), true, Map.of(tableName, 1100)); 369 370 deleteTable(tableName); 371 } 372 373 /** 374 * Replication endpoint that fails only once on first upload attempt, then succeeds on retry. 375 */ 376 public static class FailingOnceContinuousBackupReplicationEndpoint 377 extends ContinuousBackupReplicationEndpoint { 378 379 private static boolean failedOnce = false; 380 381 @Override 382 protected void uploadBulkLoadFiles(long dayInMillis, List<Path> bulkLoadFiles) 383 throws BulkLoadUploadException { 384 if (!failedOnce) { 385 failedOnce = true; 386 throw new BulkLoadUploadException("Simulated upload failure on first attempt"); 387 } 388 super.uploadBulkLoadFiles(dayInMillis, bulkLoadFiles); 389 } 390 391 /** Reset failure state for new tests */ 392 public static void reset() { 393 failedOnce = false; 394 } 395 } 396 397 /** 398 * Unit test for verifying cleanup of partial files. Simulates a failure during 399 * {@link FileUtil#copy(FileSystem, Path, FileSystem, Path, boolean, boolean, Configuration)} and 400 * checks that the destination file is deleted. 401 */ 402 @Test 403 public void testCopyWithCleanupDeletesPartialFile() throws Exception { 404 FileSystem srcFS = mock(FileSystem.class); 405 FileSystem dstFS = mock(FileSystem.class); 406 Path src = new Path("/src/file"); 407 Path dst = new Path("/dst/file"); 408 Configuration conf = new Configuration(); 409 410 FileStatus srcStatus = mock(FileStatus.class); 411 FileStatus dstStatus = mock(FileStatus.class); 412 413 when(srcFS.getFileStatus(src)).thenReturn(srcStatus); 414 when(dstFS.getFileStatus(dst)).thenReturn(dstStatus); 415 416 // lengths differ -> should attempt to overwrite and then cleanup 417 when(srcStatus.getLen()).thenReturn(200L); 418 when(dstStatus.getLen()).thenReturn(100L); 419 420 // Simulate FileUtil.copy failing 421 try (MockedStatic<FileUtil> mockedFileUtil = mockStatic(FileUtil.class)) { 422 mockedFileUtil.when( 423 () -> FileUtil.copy(eq(srcFS), eq(src), eq(dstFS), eq(dst), eq(false), eq(true), eq(conf))) 424 .thenThrow(new IOException("simulated copy failure")); 425 426 // Pretend partial file exists in destination 427 when(dstFS.exists(dst)).thenReturn(true); 428 429 // Run the method under test 430 assertThrows(IOException.class, () -> copyWithCleanup(srcFS, src, dstFS, dst, conf)); 431 432 // Verify cleanup happened 433 verify(dstFS).delete(dst, true); 434 } 435 } 436 437 /** 438 * Simulates a stale/partial file left behind after a failed bulk load. On retry, the stale file 439 * should be overwritten and replication succeeds. 440 */ 441 @Test 442 public void testBulkLoadFileUploadWithStaleFileRetry() throws Exception { 443 String methodName = Thread.currentThread().getStackTrace()[1].getMethodName(); 444 TableName tableName = TableName.valueOf("table_" + methodName); 445 String peerId = "peerId"; 446 447 // Reset static failure flag before test 448 PartiallyUploadedBulkloadFileEndpoint.reset(); 449 450 createTable(tableName); 451 452 Path backupRootDir = new Path(root, methodName); 453 fs.mkdirs(backupRootDir); 454 conf.set(CONF_BACKUP_ROOT_DIR, backupRootDir.toString()); 455 456 Map<TableName, List<String>> tableMap = new HashMap<>(); 457 tableMap.put(tableName, new ArrayList<>()); 458 459 addReplicationPeer(peerId, backupRootDir, tableMap, 460 PartiallyUploadedBulkloadFileEndpoint.class.getName()); 461 462 loadRandomData(tableName, 100); 463 assertEquals(100, getRowCount(tableName)); 464 465 Path dir = TEST_UTIL.getDataTestDirOnTestFS("testBulkLoadByFamily"); 466 generateHFiles(dir); 467 bulkLoadHFiles(tableName, dir); 468 assertEquals(1100, getRowCount(tableName)); 469 470 // first attempt will fail leaving stale file, second attempt should overwrite and succeed 471 waitForReplication(15000); 472 deleteReplicationPeer(peerId); 473 474 verifyBackup(backupRootDir.toString(), true, Map.of(tableName, 1100)); 475 476 deleteTable(tableName); 477 } 478 479 /** 480 * Replication endpoint that simulates leaving a partial file behind on first attempt, then 481 * succeeds on second attempt by overwriting it. 482 */ 483 public static class PartiallyUploadedBulkloadFileEndpoint 484 extends ContinuousBackupReplicationEndpoint { 485 486 private static boolean firstAttempt = true; 487 488 @Override 489 protected void uploadBulkLoadFiles(long dayInMillis, List<Path> bulkLoadFiles) 490 throws BulkLoadUploadException { 491 if (firstAttempt) { 492 firstAttempt = false; 493 try { 494 // Construct destination path and create a partial file 495 String dayDirectoryName = BackupUtils.formatToDateString(dayInMillis); 496 BackupFileSystemManager backupFileSystemManager = 497 new BackupFileSystemManager("peer1", conf, conf.get(CONF_BACKUP_ROOT_DIR)); 498 Path bulkloadDir = 499 new Path(backupFileSystemManager.getBulkLoadFilesDir(), dayDirectoryName); 500 501 FileSystem dstFs = backupFileSystemManager.getBackupFs(); 502 if (!dstFs.exists(bulkloadDir)) { 503 dstFs.mkdirs(bulkloadDir); 504 } 505 506 for (Path file : bulkLoadFiles) { 507 Path destPath = new Path(bulkloadDir, file); 508 try (FSDataOutputStream out = dstFs.create(destPath, true)) { 509 out.writeBytes("partial-data"); // simulate incomplete upload 510 } 511 } 512 } catch (IOException e) { 513 throw new BulkLoadUploadException("Simulated failure while creating partial file", e); 514 } 515 516 // Fail after leaving partial files behind 517 throw new BulkLoadUploadException("Simulated upload failure on first attempt"); 518 } 519 520 // Retry succeeds, overwriting stale files 521 super.uploadBulkLoadFiles(dayInMillis, bulkLoadFiles); 522 } 523 524 /** Reset for new tests */ 525 public static void reset() { 526 firstAttempt = true; 527 } 528 } 529 530 private void createTable(TableName tableName) throws IOException { 531 ColumnFamilyDescriptor columnFamilyDescriptor = 532 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CF_NAME)).setScope(1).build(); 533 TableDescriptor tableDescriptor = 534 TableDescriptorBuilder.newBuilder(tableName).setColumnFamily(columnFamilyDescriptor).build(); 535 536 if (!admin.tableExists(tableName)) { 537 admin.createTable(tableDescriptor); 538 } 539 } 540 541 private void deleteTable(TableName tableName) throws IOException { 542 admin.disableTable(tableName); 543 admin.truncateTable(tableName, false); 544 admin.disableTable(tableName); 545 admin.deleteTable(tableName); 546 } 547 548 private void addReplicationPeer(String peerId, Path backupRootDir, 549 Map<TableName, List<String>> tableMap) throws IOException { 550 addReplicationPeer(peerId, backupRootDir, tableMap, replicationEndpoint); 551 } 552 553 private void addReplicationPeer(String peerId, Path backupRootDir, 554 Map<TableName, List<String>> tableMap, String customReplicationEndpointImpl) 555 throws IOException { 556 Map<String, String> additionalArgs = new HashMap<>(); 557 additionalArgs.put(CONF_PEER_UUID, UUID.randomUUID().toString()); 558 additionalArgs.put(CONF_BACKUP_ROOT_DIR, backupRootDir.toString()); 559 560 ReplicationPeerConfig peerConfig = ReplicationPeerConfig.newBuilder() 561 .setReplicationEndpointImpl(customReplicationEndpointImpl).setReplicateAllUserTables(false) 562 .setTableCFsMap(tableMap).putAllConfiguration(additionalArgs).build(); 563 564 admin.addReplicationPeer(peerId, peerConfig); 565 } 566 567 private void deleteReplicationPeer(String peerId) throws IOException { 568 admin.disableReplicationPeer(peerId); 569 admin.removeReplicationPeer(peerId); 570 } 571 572 private void loadRandomData(TableName tableName, int totalRows) throws IOException { 573 int rowSize = 32; 574 try (Table table = TEST_UTIL.getConnection().getTable(tableName)) { 575 TEST_UTIL.loadRandomRows(table, Bytes.toBytes(CF_NAME), rowSize, totalRows); 576 } 577 } 578 579 private void bulkLoadHFiles(TableName tableName, Path inputDir) throws IOException { 580 TEST_UTIL.getConfiguration().setBoolean(BulkLoadHFilesTool.BULK_LOAD_HFILES_BY_FAMILY, true); 581 582 try (Table table = TEST_UTIL.getConnection().getTable(tableName)) { 583 BulkLoadHFiles loader = new BulkLoadHFilesTool(TEST_UTIL.getConfiguration()); 584 loader.bulkLoad(table.getName(), inputDir); 585 } finally { 586 TEST_UTIL.getConfiguration().setBoolean(BulkLoadHFilesTool.BULK_LOAD_HFILES_BY_FAMILY, false); 587 } 588 } 589 590 private void bulkLoadHFiles(TableName tableName, Map<byte[], List<Path>> family2Files) 591 throws IOException { 592 TEST_UTIL.getConfiguration().setBoolean(BulkLoadHFilesTool.BULK_LOAD_HFILES_BY_FAMILY, true); 593 594 try (Table table = TEST_UTIL.getConnection().getTable(tableName)) { 595 BulkLoadHFiles loader = new BulkLoadHFilesTool(TEST_UTIL.getConfiguration()); 596 loader.bulkLoad(table.getName(), family2Files); 597 } finally { 598 TEST_UTIL.getConfiguration().setBoolean(BulkLoadHFilesTool.BULK_LOAD_HFILES_BY_FAMILY, false); 599 } 600 } 601 602 private void generateHFiles(Path outputDir) throws IOException { 603 String hFileName = "MyHFile"; 604 int numRows = 1000; 605 outputDir = outputDir.makeQualified(fs.getUri(), fs.getWorkingDirectory()); 606 607 byte[] from = Bytes.toBytes(CF_NAME + "begin"); 608 byte[] to = Bytes.toBytes(CF_NAME + "end"); 609 610 Path familyDir = new Path(outputDir, CF_NAME); 611 HFileTestUtil.createHFile(TEST_UTIL.getConfiguration(), fs, new Path(familyDir, hFileName), 612 Bytes.toBytes(CF_NAME), QUALIFIER, from, to, numRows); 613 } 614 615 private void waitForReplication(int durationInMillis) { 616 LOG.info("Waiting for replication to complete for {} ms", durationInMillis); 617 try { 618 Thread.sleep(durationInMillis); 619 } catch (InterruptedException e) { 620 Thread.currentThread().interrupt(); 621 throw new RuntimeException("Thread was interrupted while waiting", e); 622 } 623 } 624 625 /** 626 * Verifies the backup process by: 1. Checking whether any WAL (Write-Ahead Log) files were 627 * generated in the backup directory. 2. Checking whether any bulk-loaded files were generated in 628 * the backup directory. 3. Replaying the WAL and bulk-loaded files (if present) to restore data 629 * and check consistency by verifying that the restored data matches the expected row count for 630 * each table. 631 */ 632 private void verifyBackup(String backupRootDir, boolean hasBulkLoadFiles, 633 Map<TableName, Integer> tablesWithExpectedRows) throws IOException { 634 verifyWALBackup(backupRootDir); 635 if (hasBulkLoadFiles) { 636 verifyBulkLoadBackup(backupRootDir); 637 } 638 639 for (Map.Entry<TableName, Integer> entry : tablesWithExpectedRows.entrySet()) { 640 TableName tableName = entry.getKey(); 641 int expectedRows = entry.getValue(); 642 643 admin.disableTable(tableName); 644 admin.truncateTable(tableName, false); 645 assertEquals(0, getRowCount(tableName)); 646 647 replayWALs(new Path(backupRootDir, WALS_DIR).toString(), tableName); 648 649 // replay Bulk loaded HFiles if Present 650 try { 651 Path bulkloadDir = new Path(backupRootDir, BULKLOAD_FILES_DIR); 652 if (fs.exists(bulkloadDir)) { 653 FileStatus[] directories = fs.listStatus(bulkloadDir); 654 for (FileStatus dirStatus : directories) { 655 if (dirStatus.isDirectory()) { 656 replayBulkLoadHFilesIfPresent(dirStatus.getPath().toString(), tableName); 657 } 658 } 659 } 660 } catch (Exception e) { 661 fail("Failed to replay BulkLoad HFiles properly: " + e.getMessage()); 662 } 663 664 assertEquals(expectedRows, getRowCount(tableName)); 665 } 666 } 667 668 private void verifyWALBackup(String backupRootDir) throws IOException { 669 Path walDir = new Path(backupRootDir, WALS_DIR); 670 assertTrue(fs.exists(walDir), "WAL directory does not exist!"); 671 672 RemoteIterator<LocatedFileStatus> fileStatusIterator = fs.listFiles(walDir, true); 673 List<Path> walFiles = new ArrayList<>(); 674 675 while (fileStatusIterator.hasNext()) { 676 LocatedFileStatus fileStatus = fileStatusIterator.next(); 677 Path filePath = fileStatus.getPath(); 678 679 // Check if the file starts with the expected WAL prefix 680 if (!fileStatus.isDirectory() && filePath.getName().startsWith(WAL_FILE_PREFIX)) { 681 walFiles.add(filePath); 682 } 683 } 684 685 assertNotNull(walFiles, "No WAL files found!"); 686 assertFalse(walFiles.isEmpty(), "Expected some WAL files but found none!"); 687 } 688 689 private void verifyBulkLoadBackup(String backupRootDir) throws IOException { 690 Path bulkLoadFilesDir = new Path(backupRootDir, BULKLOAD_FILES_DIR); 691 assertTrue(fs.exists(bulkLoadFilesDir), "BulkLoad Files directory does not exist!"); 692 693 FileStatus[] bulkLoadFiles = fs.listStatus(bulkLoadFilesDir); 694 assertNotNull(bulkLoadFiles, "No Bulk load files found!"); 695 assertTrue(bulkLoadFiles.length > 0, "Expected some Bulk load files but found none!"); 696 } 697 698 private void replayWALs(String walDir, TableName tableName) { 699 WALPlayer player = new WALPlayer(); 700 try { 701 assertEquals(0, ToolRunner.run(TEST_UTIL.getConfiguration(), player, 702 new String[] { walDir, tableName.getQualifierAsString() })); 703 } catch (Exception e) { 704 fail("Failed to replay WALs properly: " + e.getMessage()); 705 } 706 } 707 708 private void replayBulkLoadHFilesIfPresent(String bulkLoadDir, TableName tableName) { 709 try { 710 Path tableBulkLoadDir = new Path(bulkLoadDir + "/default/" + tableName); 711 if (fs.exists(tableBulkLoadDir)) { 712 RemoteIterator<LocatedFileStatus> fileStatusIterator = fs.listFiles(tableBulkLoadDir, true); 713 List<Path> bulkLoadFiles = new ArrayList<>(); 714 715 while (fileStatusIterator.hasNext()) { 716 LocatedFileStatus fileStatus = fileStatusIterator.next(); 717 Path filePath = fileStatus.getPath(); 718 719 if (!fileStatus.isDirectory()) { 720 bulkLoadFiles.add(filePath); 721 } 722 } 723 bulkLoadHFiles(tableName, Map.of(Bytes.toBytes(CF_NAME), bulkLoadFiles)); 724 } 725 } catch (Exception e) { 726 fail("Failed to replay BulkLoad HFiles properly: " + e.getMessage()); 727 } 728 } 729 730 private int getRowCount(TableName tableName) throws IOException { 731 try (Table table = TEST_UTIL.getConnection().getTable(tableName)) { 732 return HBaseTestingUtil.countRows(table); 733 } 734 } 735}