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