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.OPTION_ENABLE_CONTINUOUS_BACKUP;
022import static org.junit.jupiter.api.Assertions.assertEquals;
023import static org.junit.jupiter.api.Assertions.assertFalse;
024import static org.junit.jupiter.api.Assertions.assertTrue;
025
026import java.io.IOException;
027import java.util.Arrays;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.stream.Collectors;
032import org.apache.hadoop.fs.FileSystem;
033import org.apache.hadoop.fs.Path;
034import org.apache.hadoop.hbase.TableName;
035import org.apache.hadoop.hbase.backup.impl.BackupManifest;
036import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
037import org.apache.hadoop.hbase.testclassification.LargeTests;
038import org.apache.hadoop.util.ToolRunner;
039import org.junit.jupiter.api.AfterEach;
040import org.junit.jupiter.api.BeforeEach;
041import org.junit.jupiter.api.Tag;
042import org.junit.jupiter.api.Test;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046import org.apache.hbase.thirdparty.com.google.common.collect.Sets;
047
048@Tag(LargeTests.TAG)
049public class TestContinuousBackup extends TestBackupBase {
050
051  private static final Logger LOG = LoggerFactory.getLogger(TestContinuousBackup.class);
052
053  String backupWalDirName = "TestContinuousBackupWalDir";
054
055  @BeforeEach
056  public void beforeTest() throws IOException {
057    Path root = TEST_UTIL.getDataTestDirOnTestFS();
058    Path backupWalDir = new Path(root, backupWalDirName);
059    FileSystem fs = FileSystem.get(conf1);
060    fs.mkdirs(backupWalDir);
061    conf1.set(CONF_CONTINUOUS_BACKUP_WAL_DIR, backupWalDir.toString());
062  }
063
064  @AfterEach
065  public void afterTest() throws IOException {
066    Path root = TEST_UTIL.getDataTestDirOnTestFS();
067    Path backupWalDir = new Path(root, backupWalDirName);
068    FileSystem fs = FileSystem.get(conf1);
069
070    if (fs.exists(backupWalDir)) {
071      fs.delete(backupWalDir, true);
072    }
073
074    conf1.unset(CONF_CONTINUOUS_BACKUP_WAL_DIR);
075    deleteContinuousBackupReplicationPeerIfExists(TEST_UTIL.getAdmin());
076  }
077
078  @Test
079  public void testContinuousBackupWithFullBackup() throws Exception {
080    LOG.info("Testing successful continuous backup with full backup");
081    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
082    TableName tableName = TableName.valueOf("table_" + methodName);
083    TEST_UTIL.createTable(tableName, "cf");
084
085    try (BackupSystemTable table = new BackupSystemTable(TEST_UTIL.getConnection())) {
086      int before = table.getBackupHistory().size();
087
088      // Run backup
089      String[] args = buildBackupArgs("full", new TableName[] { tableName }, true);
090      int ret = ToolRunner.run(conf1, new BackupDriver(), args);
091      assertEquals(0, ret, "Backup should succeed");
092
093      // Verify backup history increased and all the backups are succeeded
094      List<BackupInfo> backups = table.getBackupHistory();
095      assertEquals(before + 1, backups.size(), "Backup history should increase");
096      for (BackupInfo data : List.of(backups.get(0))) {
097        String backupId = data.getBackupId();
098        assertTrue(checkSucceeded(backupId));
099      }
100
101      // Verify backup manifest contains the correct tables
102      BackupManifest manifest = getLatestBackupManifest(backups);
103      assertEquals(Sets.newHashSet(tableName), new HashSet<>(manifest.getTableList()),
104        "Backup should contain the expected tables");
105    }
106
107    // Verify replication peer subscription
108    BackupTestUtil.verifyReplicationPeerSubscription(TEST_UTIL, tableName);
109
110    // Verify table is registered in Backup System Table
111    verifyTableInBackupSystemTable(tableName);
112  }
113
114  @Test
115  public void testContinuousBackupForMultipleTables() throws Exception {
116    LOG.info("Test continuous backup for multiple tables");
117    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
118    TableName tableName1 = TableName.valueOf("table_" + methodName);
119    TEST_UTIL.createTable(tableName1, "cf");
120    TableName tableName2 = TableName.valueOf("table_" + methodName + "2");
121    TEST_UTIL.createTable(tableName2, "cf");
122
123    try (BackupSystemTable table = new BackupSystemTable(TEST_UTIL.getConnection())) {
124      int before = table.getBackupHistory().size();
125
126      // Create full backup for table1
127      String[] args = buildBackupArgs("full", new TableName[] { tableName1 }, true);
128      int ret = ToolRunner.run(conf1, new BackupDriver(), args);
129      assertEquals(0, ret, "Backup should succeed");
130
131      // Create full backup for table2
132      args = buildBackupArgs("full", new TableName[] { tableName2 }, true);
133      ret = ToolRunner.run(conf1, new BackupDriver(), args);
134      assertEquals(0, ret, "Backup should succeed");
135
136      // Verify backup history increased and all the backups are succeeded
137      List<BackupInfo> backups = table.getBackupHistory();
138      assertEquals(before + 2, backups.size(), "Backup history should increase");
139      for (BackupInfo data : List.of(backups.get(0), backups.get(1))) {
140        String backupId = data.getBackupId();
141        assertTrue(checkSucceeded(backupId));
142      }
143
144      // Verify backup manifest contains the correct tables
145      BackupManifest manifest = getLatestBackupManifest(backups);
146      assertEquals(Sets.newHashSet(tableName2), new HashSet<>(manifest.getTableList()),
147        "Backup should contain the expected tables");
148    }
149
150    // Verify replication peer subscription for each table
151    BackupTestUtil.verifyReplicationPeerSubscription(TEST_UTIL, tableName1);
152    BackupTestUtil.verifyReplicationPeerSubscription(TEST_UTIL, tableName2);
153
154    // Verify tables are registered in Backup System Table
155    verifyTableInBackupSystemTable(tableName1);
156    verifyTableInBackupSystemTable(tableName2);
157  }
158
159  @Test
160  public void testInvalidBackupScenarioWithContinuousEnabled() throws Exception {
161    LOG.info("Testing invalid backup scenario with continuous backup enabled");
162    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
163    TableName tableName1 = TableName.valueOf("table_" + methodName);
164    TEST_UTIL.createTable(tableName1, "cf");
165    TableName tableName2 = TableName.valueOf("table_" + methodName + "2");
166    TEST_UTIL.createTable(tableName2, "cf");
167
168    try (BackupSystemTable table = new BackupSystemTable(TEST_UTIL.getConnection())) {
169      int before = table.getBackupHistory().size();
170
171      // Create full backup for table1 with continuous backup enabled
172      String[] args = buildBackupArgs("full", new TableName[] { tableName1 }, true);
173      int ret = ToolRunner.run(conf1, new BackupDriver(), args);
174      assertEquals(0, ret, "Backup should succeed");
175
176      // Create full backup for table2 without continuous backup enabled
177      args = buildBackupArgs("full", new TableName[] { tableName2 }, false);
178      ret = ToolRunner.run(conf1, new BackupDriver(), args);
179      assertEquals(0, ret, "Backup should succeed");
180
181      // Attempt full backup for both tables without continuous backup enabled (should fail)
182      args = buildBackupArgs("full", new TableName[] { tableName1, tableName2 }, false);
183      ret = ToolRunner.run(conf1, new BackupDriver(), args);
184      assertTrue(ret != 0, "Backup should fail due to mismatch in continuous backup settings");
185
186      // Verify backup history size is unchanged after the failed backup
187      int after = table.getBackupHistory().size();
188      assertEquals(before + 2, after, "Backup history should remain unchanged on failure");
189    }
190  }
191
192  @Test
193  public void testContinuousBackupWithWALDirNotSpecified() throws Exception {
194    LOG.info("Testing that continuous backup fails when WAL directory is not specified");
195    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
196    TableName tableName = TableName.valueOf("table_" + methodName);
197    TEST_UTIL.createTable(tableName, "cf");
198
199    conf1.unset(CONF_CONTINUOUS_BACKUP_WAL_DIR);
200    LOG.info("CONF_CONTINUOUS_BACKUP_WAL_DIR: {}", conf1.get(CONF_CONTINUOUS_BACKUP_WAL_DIR));
201
202    try (BackupSystemTable table = new BackupSystemTable(TEST_UTIL.getConnection())) {
203      int before = table.getBackupHistory().size();
204
205      // Run full backup without specifying WAL directory (invalid scenario)
206      String[] args = buildBackupArgs("full", new TableName[] { tableName }, true);
207      int ret = ToolRunner.run(conf1, new BackupDriver(), args);
208
209      assertTrue(ret != 0, "Backup should fail when WAL directory is not specified");
210
211      List<BackupInfo> backups = table.getBackupHistory();
212      int after = backups.size();
213      assertEquals(before + 1, after, "Backup history should increase");
214
215      // last backup should be a failure
216      assertFalse(checkSucceeded(backups.get(0).getBackupId()));
217    }
218  }
219
220  @Test
221  public void testContinuousBackupWithIncrementalBackup() throws Exception {
222    LOG.info("Testing that continuous backup cannot be enabled with incremental backup");
223    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
224    TableName tableName = TableName.valueOf("table_" + methodName);
225    TEST_UTIL.createTable(tableName, "cf");
226
227    try (BackupSystemTable table = new BackupSystemTable(TEST_UTIL.getConnection())) {
228      int before = table.getBackupHistory().size();
229
230      // Run incremental backup with continuous backup flag (invalid scenario)
231      String[] args = buildBackupArgs("incremental", new TableName[] { tableName }, true);
232      int ret = ToolRunner.run(conf1, new BackupDriver(), args);
233
234      assertTrue(ret != 0, "Backup should fail when using continuous backup with incremental mode");
235
236      // Backup history should remain unchanged
237      int after = table.getBackupHistory().size();
238      assertEquals(before, after, "Backup history should remain unchanged on failure");
239    }
240  }
241
242  String[] buildBackupArgs(String backupType, TableName[] tables, boolean continuousEnabled) {
243    String tableNames =
244      Arrays.stream(tables).map(TableName::getNameAsString).collect(Collectors.joining(","));
245
246    if (continuousEnabled) {
247      return new String[] { "create", backupType, BACKUP_ROOT_DIR, "-t", tableNames,
248        "-" + OPTION_ENABLE_CONTINUOUS_BACKUP };
249    } else {
250      return new String[] { "create", backupType, BACKUP_ROOT_DIR, "-t", tableNames };
251    }
252  }
253
254  private void verifyTableInBackupSystemTable(TableName table) throws IOException {
255    try (BackupSystemTable backupTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
256      Map<TableName, Long> tableBackupMap = backupTable.getContinuousBackupTableSet();
257
258      assertTrue(tableBackupMap.containsKey(table),
259        "Table is missing in the continuous backup table set");
260
261      assertTrue(tableBackupMap.get(table) > 0, "Timestamp for table should be greater than 0");
262    }
263  }
264
265}