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      assertFalse(fs.exists(backupTmpDir),
142        "Temporary backup directory " + backupTmpDir + " should have been deleted");
143
144      // Verify backup history increased and all the backups are succeeded
145      backups = backupSystemTable.getBackupHistory();
146      assertEquals(before + 1, backups.size(), "Backup history should increase");
147
148      String originalTableChecksum = TEST_UTIL.checksumRows(t1);
149
150      LOG.info("Truncating table: {}", tableName);
151      TEST_UTIL.truncateTable(tableName);
152
153      // Restore incremental backup
154      TableName[] tables = new TableName[] { tableName };
155      BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection());
156      LOG.info("Restoring table: {}", tableName);
157      // In the restore request, the original table is both the "from table" and the "to table".
158      // This means the table is being restored "into itself". It is not being restored into
159      // separate table.
160      client.restore(
161        BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true));
162
163      LOG.info("Verifying data integrity for restored table: {}", tableName);
164      verifyRestoredTableDataIntegrity(tables[0], originalTableChecksum, NB_ROWS_IN_BATCH);
165    }
166  }
167
168  @Test
169  public void testMultiTableContinuousBackupWithIncrementalBackupSuccess() throws Exception {
170    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
171    List<Table> tables = new ArrayList<>();
172    List<TableName> tableNames = new ArrayList<>();
173    tableNames.add(TableName.valueOf("table_" + methodName + "_0"));
174    tableNames.add(TableName.valueOf("table_" + methodName + "_1"));
175    tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_0"));
176    tableNames.add(TableName.valueOf("ns1", "ns1_table_" + methodName + "_1"));
177    tableNames.add(TableName.valueOf("sameTableNameDifferentNamespace"));
178    tableNames.add(TableName.valueOf("ns3", "sameTableNameDifferentNamespace"));
179
180    for (TableName table : tableNames) {
181      LOG.info("Creating table: {}", table);
182      tables.add(TEST_UTIL.createTable(table, famName));
183    }
184
185    try (BackupSystemTable backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
186      int before = backupSystemTable.getBackupHistory().size();
187
188      // Run continuous backup on multiple tables
189      LOG.info("Running full backup with continuous backup enabled on tables: {}", tableNames);
190      String backup1 = backupTables(BackupType.FULL, tableNames, BACKUP_ROOT_DIR, true);
191      LOG.info("Full backup complete with ID {} for tables: {}", backup1, tableNames);
192      assertTrue(checkSucceeded(backup1));
193
194      // Verify backup history increased and all backups have succeeded
195      LOG.info("Verify backup history increased and all backups have succeeded");
196      List<BackupInfo> backups = backupSystemTable.getBackupHistory();
197      assertEquals(before + 1, backups.size(), "Backup history should increase");
198
199      // Verify backup manifest contains the correct tables
200      LOG.info("Verify backup manifest contains the correct tables");
201      BackupManifest manifest = getLatestBackupManifest(backups);
202      assertEquals(Sets.newHashSet(tableNames), new HashSet<>(manifest.getTableList()),
203        "Backup should contain the expected tables");
204
205      loadTables(tables);
206      Thread.sleep(10000);
207
208      // Run incremental backup
209      LOG.info("Running incremental backup on tables: {}", tableNames);
210      before = backupSystemTable.getBackupHistory().size();
211      String backup2 = backupTables(BackupType.INCREMENTAL, tableNames, BACKUP_ROOT_DIR, true);
212      assertTrue(checkSucceeded(backup2));
213      LOG.info("Incremental backup completed with ID {} for tables: {}", backup2, tableNames);
214
215      // Verify backup history increased and all the backups are succeeded
216      backups = backupSystemTable.getBackupHistory();
217      assertEquals(before + 1, backups.size(), "Backup history should increase");
218
219      // We need to get each table's original row checksum before truncating each table
220      LinkedHashMap<TableName, String> originalTableChecksums = new LinkedHashMap<>();
221      for (Table table : tables) {
222        LOG.info("Getting row checksum for table: {}", table);
223        originalTableChecksums.put(table.getName(), TEST_UTIL.checksumRows(table));
224      }
225
226      for (TableName tableName : tableNames) {
227        LOG.info("Truncating table: {}", tableName);
228        TEST_UTIL.truncateTable(tableName);
229      }
230
231      // Restore incremental backup
232      TableName[] tableNamesArray = tableNames.toArray(new TableName[0]);
233      BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection());
234      LOG.info("Restoring tables: {}", tableNames);
235      // In the restore request, the original tables are both the list of "from tables" and the
236      // list of "to tables". This means the tables are being restored "into themselves". They are
237      // not being restored into separate tables.
238      client.restore(BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false,
239        tableNamesArray, tableNamesArray, true));
240
241      for (TableName tableName : originalTableChecksums.keySet()) {
242        LOG.info("Verifying data integrity for restored table: {}", tableName);
243        verifyRestoredTableDataIntegrity(tableName, originalTableChecksums.get(tableName),
244          NB_ROWS_IN_BATCH);
245      }
246    }
247  }
248
249  @Test
250  public void testIncrementalBackupCopyingBulkloadTillIncrCommittedWalTs() throws Exception {
251    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
252    TableName tableName1 = TableName.valueOf("table_" + methodName);
253    TEST_UTIL.createTable(tableName1, famName);
254
255    try (BackupSystemTable systemTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
256      // The test starts with no data, and no bulk loaded rows.
257      int expectedRowCount = 0;
258      assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1));
259      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
260
261      // Create continuous backup, bulk loads are now being tracked
262      String backup1 = backupTables(BackupType.FULL, List.of(tableName1), BACKUP_ROOT_DIR, true);
263      assertTrue(checkSucceeded(backup1));
264
265      loadTable(TEST_UTIL.getConnection().getTable(tableName1));
266      expectedRowCount = expectedRowCount + NB_ROWS_IN_BATCH;
267      performBulkLoad("bulkPreIncr", methodName, tableName1);
268      expectedRowCount += ROWS_IN_BULK_LOAD;
269      assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1));
270      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
271      loadTable(TEST_UTIL.getConnection().getTable(tableName1));
272      Thread.sleep(15000);
273
274      performBulkLoad("bulkPostIncr", methodName, tableName1);
275      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
276
277      // Incremental backup
278      String backup2 =
279        backupTables(BackupType.INCREMENTAL, List.of(tableName1), BACKUP_ROOT_DIR, true);
280      assertTrue(checkSucceeded(backup2));
281      assertTrue(systemTable.readBulkloadRows(List.of(tableName1)).isEmpty());
282
283      TEST_UTIL.truncateTable(tableName1);
284      // Restore incremental backup
285      TableName[] tables = new TableName[] { tableName1 };
286      BackupAdminImpl client = new BackupAdminImpl(TEST_UTIL.getConnection());
287      client.restore(
288        BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backup2, false, tables, tables, true));
289      assertEquals(expectedRowCount, TEST_UTIL.countRows(tableName1));
290    }
291  }
292
293  private void performBulkLoad(String keyPrefix, String testDir, TableName tableName)
294    throws IOException {
295    FileSystem fs = TEST_UTIL.getTestFileSystem();
296    Path baseDirectory = TEST_UTIL.getDataTestDirOnTestFS(testDir);
297    Path hfilePath =
298      new Path(baseDirectory, Bytes.toString(famName) + Path.SEPARATOR + "hfile_" + keyPrefix);
299
300    HFileTestUtil.createHFile(TEST_UTIL.getConfiguration(), fs, hfilePath, famName, qualName,
301      Bytes.toBytes(keyPrefix), Bytes.toBytes(keyPrefix + "z"), ROWS_IN_BULK_LOAD);
302
303    listFiles(fs, baseDirectory, baseDirectory);
304
305    Map<BulkLoadHFiles.LoadQueueItem, ByteBuffer> result =
306      BulkLoadHFiles.create(TEST_UTIL.getConfiguration()).bulkLoad(tableName, baseDirectory);
307    assertFalse(result.isEmpty());
308  }
309
310  private static Set<String> listFiles(final FileSystem fs, final Path root, final Path dir)
311    throws IOException {
312    Set<String> files = new HashSet<>();
313    FileStatus[] list = CommonFSUtils.listStatus(fs, dir);
314    if (list != null) {
315      for (FileStatus fstat : list) {
316        if (fstat.isDirectory()) {
317          LOG.info("Found directory {}", Objects.toString(fstat.getPath()));
318          files.addAll(listFiles(fs, root, fstat.getPath()));
319        } else {
320          LOG.info("Found file {}", Objects.toString(fstat.getPath()));
321          String file = fstat.getPath().makeQualified(fs).toString();
322          files.add(file);
323        }
324      }
325    }
326    return files;
327  }
328
329  protected static void loadTable(Table table) throws Exception {
330    Put p; // 100 + 1 row to t1_syncup
331    for (int i = 0; i < NB_ROWS_IN_BATCH; i++) {
332      p = new Put(Bytes.toBytes("rowLoad" + i));
333      p.addColumn(famName, qualName, Bytes.toBytes("val" + i));
334      table.put(p);
335    }
336  }
337
338  protected static void loadTables(List<Table> tables) throws Exception {
339    for (Table table : tables) {
340      LOG.info("Loading data into table: {}", table);
341      loadTable(table);
342    }
343  }
344
345  private void verifyRestoredTableDataIntegrity(TableName restoredTableName,
346    String originalTableChecksum, int expectedRowCount) throws Exception {
347    try (Table restoredTable = TEST_UTIL.getConnection().getTable(restoredTableName);
348      ResultScanner scanner = restoredTable.getScanner(new Scan())) {
349
350      // Verify the checksum for the original table (before it was truncated) matches the checksum
351      // of the restored table.
352      String restoredTableChecksum = TEST_UTIL.checksumRows(restoredTable);
353      assertEquals(originalTableChecksum, restoredTableChecksum,
354        "The restored table's row checksum did not match the original table's checksum");
355
356      // Verify the data in the restored table is the same as when it was originally loaded
357      // into the table.
358      int count = 0;
359      for (Result result : scanner) {
360        // The data has a numerical match between its row key and value (such as rowLoad1 and
361        // value1)
362        // We can use this to ensure a row key has the expected value.
363        String rowKey = Bytes.toString(result.getRow());
364        int index = Integer.parseInt(rowKey.replace("rowLoad", ""));
365
366        // Verify the Value
367        byte[] actualValue = result.getValue(famName, qualName);
368        assertNotNull(actualValue, "Value missing for row key: " + rowKey);
369        String expectedValue = "val" + index;
370        assertEquals(expectedValue, Bytes.toString(actualValue),
371          "Value mismatch for row key: " + rowKey);
372
373        count++;
374      }
375      assertEquals(expectedRowCount, count);
376    }
377  }
378}