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}