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}