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;
019
020import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR;
021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS;
022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD;
023import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_DEFAULT;
024import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_KEY;
025import static org.junit.jupiter.api.Assertions.assertEquals;
026import static org.junit.jupiter.api.Assertions.assertFalse;
027import static org.junit.jupiter.api.Assertions.assertNotNull;
028import static org.junit.jupiter.api.Assertions.assertTrue;
029
030import java.io.IOException;
031import java.nio.ByteBuffer;
032import java.util.ArrayList;
033import java.util.HashSet;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Objects;
038import java.util.Set;
039import org.apache.hadoop.fs.FileStatus;
040import org.apache.hadoop.fs.FileSystem;
041import org.apache.hadoop.fs.Path;
042import org.apache.hadoop.hbase.TableName;
043import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl;
044import org.apache.hadoop.hbase.backup.impl.BackupManifest;
045import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
046import org.apache.hadoop.hbase.backup.util.BackupUtils;
047import org.apache.hadoop.hbase.client.Put;
048import org.apache.hadoop.hbase.client.Result;
049import org.apache.hadoop.hbase.client.ResultScanner;
050import org.apache.hadoop.hbase.client.Scan;
051import org.apache.hadoop.hbase.client.Table;
052import org.apache.hadoop.hbase.testclassification.LargeTests;
053import org.apache.hadoop.hbase.tool.BulkLoadHFiles;
054import org.apache.hadoop.hbase.util.Bytes;
055import org.apache.hadoop.hbase.util.CommonFSUtils;
056import org.apache.hadoop.hbase.util.HFileTestUtil;
057import org.junit.jupiter.api.AfterEach;
058import org.junit.jupiter.api.BeforeEach;
059import org.junit.jupiter.api.Tag;
060import org.junit.jupiter.api.Test;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064import org.apache.hbase.thirdparty.com.google.common.collect.Sets;
065
066@Tag(LargeTests.TAG)
067public class TestIncrementalBackupWithContinuous extends TestBackupBase {
068
069  private static final Logger LOG =
070    LoggerFactory.getLogger(TestIncrementalBackupWithContinuous.class);
071
072  private static final int ROWS_IN_BULK_LOAD = 100;
073  private static final String backupWalDirName = "TestContinuousBackupWalDir";
074
075  private FileSystem fs;
076
077  @BeforeEach
078  public void beforeTest() throws IOException {
079    Path root = TEST_UTIL.getDataTestDirOnTestFS();
080    Path backupWalDir = new Path(root, backupWalDirName);
081    conf1.set(CONF_CONTINUOUS_BACKUP_WAL_DIR, backupWalDir.toString());
082    conf1.setBoolean(REPLICATION_MARKER_ENABLED_KEY, true);
083    conf1.setLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS, 2000L); // 2 seconds
084    conf1.setLong(CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD, 1024L); // 1 KB
085    fs = FileSystem.get(conf1);
086  }
087
088  @AfterEach
089  public void afterTest() throws IOException {
090    Path root = TEST_UTIL.getDataTestDirOnTestFS();
091    Path backupWalDir = new Path(root, backupWalDirName);
092    if (fs.exists(backupWalDir)) {
093      fs.delete(backupWalDir, true);
094    }
095    conf1.unset(CONTINUOUS_BACKUP_OFFSET_UPDATE_INTERVAL_MS);
096    conf1.unset(CONTINUOUS_BACKUP_OFFSET_UPDATE_SIZE_THRESHOLD);
097    conf1.unset(CONF_CONTINUOUS_BACKUP_WAL_DIR);
098    conf1.setBoolean(REPLICATION_MARKER_ENABLED_KEY, REPLICATION_MARKER_ENABLED_DEFAULT);
099    deleteContinuousBackupReplicationPeerIfExists(TEST_UTIL.getAdmin());
100  }
101
102  @Test
103  public void testContinuousBackupWithIncrementalBackupSuccess() throws Exception {
104    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
105    TableName tableName = TableName.valueOf("table_" + methodName);
106    Table t1 = TEST_UTIL.createTable(tableName, famName);
107
108    try (BackupSystemTable backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
109      int before = backupSystemTable.getBackupHistory().size();
110
111      // Run continuous backup
112      LOG.info("Running full backup with continuous backup enabled on table: {}", tableName);
113      String backup1 = backupTables(BackupType.FULL, List.of(tableName), BACKUP_ROOT_DIR, true);
114      LOG.info("Full backup complete with ID {} for table: {}", backup1, tableName);
115      assertTrue(checkSucceeded(backup1));
116
117      // Verify backup history increased and all the backups are succeeded
118      LOG.info("Verify backup history increased and all the backups are succeeded");
119      List<BackupInfo> backups = backupSystemTable.getBackupHistory();
120      assertEquals(before + 1, backups.size(), "Backup history should increase");
121
122      // Verify backup manifest contains the correct tables
123      LOG.info("Verify backup manifest contains the correct tables");
124      BackupManifest manifest = getLatestBackupManifest(backups);
125      assertEquals(Sets.newHashSet(tableName), new HashSet<>(manifest.getTableList()),
126        "Backup should contain the expected tables");
127
128      loadTable(t1);
129      Thread.sleep(10000);
130
131      // Run incremental backup
132      LOG.info("Run incremental backup now on table: {}", tableName);
133      before = backupSystemTable.getBackupHistory().size();
134      String backup2 =
135        backupTables(BackupType.INCREMENTAL, List.of(tableName), BACKUP_ROOT_DIR, true);
136      assertTrue(checkSucceeded(backup2));
137      LOG.info("Incremental backup completed for table: {}", tableName);
138
139      // Verify the temporary backup directory was deleted
140      Path backupTmpDir = new Path(BACKUP_ROOT_DIR, ".tmp");
141      Path bulkLoadOutputDir = new Path(backupTmpDir, backup2);
142      assertFalse(fs.exists(bulkLoadOutputDir),
143        "Bulk load output directory " + bulkLoadOutputDir + " should have been deleted");
144
145      // Verify backup history increased and all the backups are succeeded
146      backups = backupSystemTable.getBackupHistory();
147      assertEquals(before + 1, backups.size(), "Backup history should increase");
148
149      String originalTableChecksum = TEST_UTIL.checksumRows(t1);
150
151      LOG.info("Truncating table: {}", tableName);
152      TEST_UTIL.truncateTable(tableName);
153
154      // Restore incremental backup
155      TableName[] tables = new TableName[] { tableName };
156      BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection());
157      LOG.info("Restoring table: {}", tableName);
158      // In the restore request, the original table is both the "from table" and the "to table".
159      // This means the table is being restored "into itself". It is not being restored into
160      // separate table.
161      client.restore(
162        BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true));
163
164      LOG.info("Verifying data integrity for restored table: {}", tableName);
165      verifyRestoredTableDataIntegrity(tables[0], originalTableChecksum, NB_ROWS_IN_BATCH);
166    }
167  }
168
169  @Test
170  public void testMultiTableContinuousBackupWithIncrementalBackupSuccess() throws Exception {
171    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
172    List<Table> tables = new ArrayList<>();
173    List<TableName> tableNames = new ArrayList<>();
174    tableNames.add(TableName.valueOf("table_" + methodName + "_0"));
175    tableNames.add(TableName.valueOf("table_" + methodName + "_1"));
176    tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_0"));
177    tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_1"));
178    tableNames.add(TableName.valueOf("sameTableNameDifferentNamespace"));
179    tableNames.add(TableName.valueOf("ns3", "sameTableNameDifferentNamespace"));
180
181    for (TableName table : tableNames) {
182      LOG.info("Creating table: {}", table);
183      tables.add(TEST_UTIL.createTable(table, famName));
184    }
185
186    try (BackupSystemTable backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
187      int before = backupSystemTable.getBackupHistory().size();
188
189      // Run continuous backup on multiple tables
190      LOG.info("Running full backup with continuous backup enabled on tables: {}", tableNames);
191      String backup1 = backupTables(BackupType.FULL, tableNames, BACKUP_ROOT_DIR, true);
192      LOG.info("Full backup complete with ID {} for tables: {}", backup1, tableNames);
193      assertTrue(checkSucceeded(backup1));
194
195      // Verify backup history increased and all backups have succeeded
196      LOG.info("Verify backup history increased and all backups have succeeded");
197      List<BackupInfo> backups = backupSystemTable.getBackupHistory();
198      assertEquals(before + 1, backups.size(), "Backup history should increase");
199
200      // Verify backup manifest contains the correct tables
201      LOG.info("Verify backup manifest contains the correct tables");
202      BackupManifest manifest = getLatestBackupManifest(backups);
203      assertEquals(Sets.newHashSet(tableNames), new HashSet<>(manifest.getTableList()),
204        "Backup should contain the expected tables");
205
206      loadTables(tables);
207      Thread.sleep(10000);
208
209      // Run incremental backup
210      LOG.info("Running incremental backup on tables: {}", tableNames);
211      before = backupSystemTable.getBackupHistory().size();
212      String backup2 = backupTables(BackupType.INCREMENTAL, tableNames, BACKUP_ROOT_DIR, true);
213      assertTrue(checkSucceeded(backup2));
214      LOG.info("Incremental backup completed with ID {} for tables: {}", backup2, tableNames);
215
216      // Verify backup history increased and all the backups are succeeded
217      backups = backupSystemTable.getBackupHistory();
218      assertEquals(before + 1, backups.size(), "Backup history should increase");
219
220      // We need to get each table's original row checksum before truncating each table
221      LinkedHashMap<TableName, String> originalTableChecksums = new LinkedHashMap<>();
222      for (Table table : tables) {
223        LOG.info("Getting row checksum for table: {}", table);
224        originalTableChecksums.put(table.getName(), TEST_UTIL.checksumRows(table));
225      }
226
227      for (TableName tableName : tableNames) {
228        LOG.info("Truncating table: {}", tableName);
229        TEST_UTIL.truncateTable(tableName);
230      }
231
232      // Restore incremental backup
233      TableName[] tableNamesArray = tableNames.toArray(new TableName[0]);
234      BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection());
235      LOG.info("Restoring tables: {}", tableNames);
236      // In the restore request, the original tables are both the list of "from tables" and the
237      // list of "to tables". This means the tables are being restored "into themselves". They are
238      // not being restored into separate tables.
239      client.restore(BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false,
240        tableNamesArray, tableNamesArray, true));
241
242      for (TableName tableName : originalTableChecksums.keySet()) {
243        LOG.info("Verifying data integrity for restored table: {}", tableName);
244        verifyRestoredTableDataIntegrity(tableName, originalTableChecksums.get(tableName),
245          NB_ROWS_IN_BATCH);
246      }
247    }
248  }
249
250  @Test
251  public void testIncrementalBackupCopyingBulkloadTillIncrCommittedWalTs() throws Exception {
252    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
253    TableName tableName1 = TableName.valueOf("table_" + methodName);
254    TEST_UTIL.createTable(tableName1, famName);
255
256    try (BackupSystemTable systemTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
257      // The test starts with no data, and no bulk loaded rows.
258      int expectedRowCount = 0;
259      assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1));
260      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
261
262      // Create continuous backup, bulk loads are now being tracked
263      String backup1 = backupTables(BackupType.FULL, List.of(tableName1), BACKUP_ROOT_DIR, true);
264      assertTrue(checkSucceeded(backup1));
265
266      loadTable(TEST_UTIL.getConnection().getTable(tableName1));
267      expectedRowCount = expectedRowCount + NB_ROWS_IN_BATCH;
268      performBulkLoad("bulkPreIncr", methodName, tableName1);
269      expectedRowCount += ROWS_IN_BULK_LOAD;
270      assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1));
271      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
272      loadTable(TEST_UTIL.getConnection().getTable(tableName1));
273      Thread.sleep(15000);
274
275      performBulkLoad("bulkPostIncr", methodName, tableName1);
276      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
277
278      // Incremental backup
279      String backup2 =
280        backupTables(BackupType.INCREMENTAL, List.of(tableName1), BACKUP_ROOT_DIR, true);
281      assertTrue(checkSucceeded(backup2));
282      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
283
284      TEST_UTIL.truncateTable(tableName1);
285      // Restore incremental backup
286      TableName[] tables = new TableName[] { tableName1 };
287      BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection());
288      client.restore(
289        BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true));
290      assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1));
291    }
292  }
293
294  private void performBulkLoad(String keyPrefix, String testDir, TableName tableName)
295    throws IOException {
296    FileSystem fs = TEST_UTIL.getTestFileSystem();
297    Path baseDirectory = TEST_UTIL.getDataTestDirOnTestFS(testDir);
298    Path hfilePath =
299      new Path(baseDirectory, Bytes.toString(famName) + Path.SEPARATOR + "hfile_" + keyPrefix);
300
301    HFileTestUtil.createHFile(TEST_UTIL.getConfiguration(), fs, hfilePath, famName, qualName,
302      Bytes.toBytes(keyPrefix), Bytes.toBytes(keyPrefix + "z"), ROWS_IN_BULK_LOAD);
303
304    listFiles(fs, baseDirectory, baseDirectory);
305
306    Map<BulkLoadHFiles.LoadQueueItem, ByteBuffer> result =
307      BulkLoadHFiles.create(TEST_UTIL.getConfiguration()).bulkLoad(tableName, baseDirectory);
308    assertFalse(result.isEmpty());
309  }
310
311  private static Set<String> listFiles(final FileSystem fs, final Path root, final Path dir)
312    throws IOException {
313    Set<String> files = new HashSet<>();
314    FileStatus[] list = CommonFSUtils.listStatus(fs, dir);
315    if (list != null) {
316      for (FileStatus fstat : list) {
317        if (fstat.isDirectory()) {
318          LOG.info("Found directory {}", Objects.toString(fstat.getPath()));
319          files.addAll(listFiles(fs, root, fstat.getPath()));
320        } else {
321          LOG.info("Found file {}", Objects.toString(fstat.getPath()));
322          String file = fstat.getPath().makeQualified(fs).toString();
323          files.add(file);
324        }
325      }
326    }
327    return files;
328  }
329
330  protected static void loadTable(Table table) throws Exception {
331    Put p; // 100 + 1 row to t1_syncup
332    for (int i = 0; i < NB_ROWS_IN_BATCH; i++) {
333      p = new Put(Bytes.toBytes("rowLoad" + i));
334      p.addColumn(famName, qualName, Bytes.toBytes("val" + i));
335      table.put(p);
336    }
337  }
338
339  protected static void loadTables(List<Table> tables) throws Exception {
340    for (Table table : tables) {
341      LOG.info("Loading data into table: {}", table);
342      loadTable(table);
343    }
344  }
345
346  private void verifyRestoredTableDataIntegrity(TableName restoredTableName,
347    String originalTableChecksum, int expectedRowCount) throws Exception {
348    try (Table restoredTable = TEST_UTIL.getConnection().getTable(restoredTableName);
349      ResultScanner scanner = restoredTable.getScanner(new Scan())) {
350
351      // Verify the checksum for the original table (before it was truncated) matches the checksum
352      // of the restored table.
353      String restoredTableChecksum = TEST_UTIL.checksumRows(restoredTable);
354      assertEquals(originalTableChecksum, restoredTableChecksum,
355        "The restored table's row checksum did not match the original table's checksum");
356
357      // Verify the data in the restored table is the same as when it was originally loaded
358      // into the table.
359      int count = 0;
360      for (Result result : scanner) {
361        // The data has a numerical match between its row key and value (such as rowLoad1 and
362        // value1)
363        // We can use this to ensure a row key has the expected value.
364        String rowKey = Bytes.toString(result.getRow());
365        int index = Integer.parseInt(rowKey.replace("rowLoad", ""));
366
367        // Verify the Value
368        byte[] actualValue = result.getValue(famName, qualName);
369        assertNotNull(actualValue, "Value missing for row key: " + rowKey);
370        String expectedValue = "val" + index;
371        assertEquals(expectedValue, Bytes.toString(actualValue),
372          "Value mismatch for row key: " + rowKey);
373
374        count++;
375      }
376      assertEquals(expectedRowCount, count);
377    }
378  }
379}