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.replication.ContinuousBackupReplicationEndpoint.ONE_DAY_IN_MILLISECONDS; 023import static org.apache.hadoop.hbase.backup.util.BackupFileSystemManager.BULKLOAD_FILES_DIR; 024import static org.apache.hadoop.hbase.backup.util.BackupFileSystemManager.WALS_DIR; 025import static org.apache.hadoop.hbase.backup.util.BackupUtils.DATE_FORMAT; 026import static org.junit.jupiter.api.Assertions.assertEquals; 027import static org.junit.jupiter.api.Assertions.assertFalse; 028import static org.junit.jupiter.api.Assertions.assertTrue; 029 030import java.io.IOException; 031import java.text.SimpleDateFormat; 032import java.util.ArrayList; 033import java.util.Date; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037import org.apache.hadoop.conf.Configuration; 038import org.apache.hadoop.fs.FileStatus; 039import org.apache.hadoop.fs.FileSystem; 040import org.apache.hadoop.fs.Path; 041import org.apache.hadoop.hbase.TableName; 042import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl; 043import org.apache.hadoop.hbase.backup.impl.BackupSystemTable; 044import org.apache.hadoop.hbase.backup.util.BackupFileSystemManager; 045import org.apache.hadoop.hbase.testclassification.LargeTests; 046import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 047import org.apache.hadoop.util.ToolRunner; 048import org.junit.jupiter.api.AfterEach; 049import org.junit.jupiter.api.BeforeEach; 050import org.junit.jupiter.api.Tag; 051import org.junit.jupiter.api.Test; 052 053import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 054 055@Tag(LargeTests.TAG) 056public class TestBackupDeleteWithCleanup extends TestBackupBase { 057 058 String backupWalDirName = "TestBackupDeleteWithCleanup"; 059 060 private FileSystem fs; 061 private Path backupWalDir; 062 private BackupSystemTable backupSystemTable; 063 064 @BeforeEach 065 public void setUpTest() throws Exception { 066 Path root = TEST_UTIL.getDataTestDirOnTestFS(); 067 backupWalDir = new Path(root, backupWalDirName); 068 conf1.set(CONF_CONTINUOUS_BACKUP_WAL_DIR, backupWalDir.toString()); 069 fs = FileSystem.get(conf1); 070 fs.mkdirs(backupWalDir); 071 backupSystemTable = new BackupSystemTable(TEST_UTIL.getConnection()); 072 } 073 074 @AfterEach 075 public void tearDownTest() throws Exception { 076 if (backupSystemTable != null) { 077 backupSystemTable.close(); 078 } 079 if (fs != null && backupWalDir != null) { 080 fs.delete(backupWalDir, true); 081 } 082 083 EnvironmentEdgeManager.reset(); 084 } 085 086 @Test 087 public void testBackupDeleteWithCleanupLogic() throws Exception { 088 // Step 1: Setup Backup Folders 089 long currentTime = EnvironmentEdgeManager.getDelegate().currentTime(); 090 setupBackupFolders(currentTime); 091 092 // Log the directory structure before cleanup 093 logDirectoryStructure(fs, backupWalDir, "Directory structure BEFORE cleanup:"); 094 095 // Step 2: Simulate Backup Creation 096 backupSystemTable.addContinuousBackupTableSet(Set.of(table1), 097 currentTime - (2 * ONE_DAY_IN_MILLISECONDS)); 098 099 EnvironmentEdgeManager 100 .injectEdge(() -> System.currentTimeMillis() - (2 * ONE_DAY_IN_MILLISECONDS)); 101 102 String backupId = fullTableBackup(Lists.newArrayList(table1)); 103 assertTrue(checkSucceeded(backupId)); 104 String anotherBackupId = fullTableBackup(Lists.newArrayList(table1)); 105 assertTrue(checkSucceeded(anotherBackupId)); 106 107 // Step 3: Run Delete Command 108 deleteBackup(backupId); 109 110 // Log the directory structure after cleanup 111 logDirectoryStructure(fs, backupWalDir, "Directory structure AFTER cleanup:"); 112 113 // Step 4: Verify Cleanup 114 verifyBackupCleanup(fs, backupWalDir, currentTime); 115 116 // Step 5: Verify System Table Update 117 verifySystemTableUpdate(backupSystemTable, currentTime); 118 119 // Cleanup 120 deleteBackup(anotherBackupId); 121 } 122 123 @Test 124 public void testSingleBackupForceDelete() throws Exception { 125 // Step 1: Setup Backup Folders 126 long currentTime = EnvironmentEdgeManager.getDelegate().currentTime(); 127 setupBackupFolders(currentTime); 128 129 // Log the directory structure before cleanup 130 logDirectoryStructure(fs, backupWalDir, "Directory structure BEFORE cleanup:"); 131 132 // Step 2: Simulate Backup Creation 133 backupSystemTable.addContinuousBackupTableSet(Set.of(table1), 134 currentTime - (2 * ONE_DAY_IN_MILLISECONDS)); 135 136 EnvironmentEdgeManager 137 .injectEdge(() -> System.currentTimeMillis() - (2 * ONE_DAY_IN_MILLISECONDS)); 138 139 String backupId = fullTableBackupWithContinuous(Lists.newArrayList(table1)); 140 assertTrue(checkSucceeded(backupId)); 141 142 assertTrue(continuousBackupReplicationPeerExistsAndEnabled(), 143 "Backup replication peer should be enabled after the backup"); 144 145 // Step 3: Run Delete Command 146 deleteBackup(backupId); 147 148 // Log the directory structure after cleanup 149 logDirectoryStructure(fs, backupWalDir, "Directory structure AFTER cleanup:"); 150 151 // Step 4: Verify CONTINUOUS_BACKUP_REPLICATION_PEER is disabled 152 assertFalse(continuousBackupReplicationPeerExistsAndEnabled(), 153 "Backup replication peer should be disabled or removed"); 154 155 // Step 5: Verify that system table is updated to remove all the tables 156 Set<TableName> remainingTables = backupSystemTable.getContinuousBackupTableSet().keySet(); 157 assertTrue(remainingTables.isEmpty(), 158 "System table should have no tables after all full backups are clear"); 159 160 // Step 6: Verify that the backup WAL directory is empty 161 assertTrue(areWalAndBulkloadDirsEmpty(conf1, backupWalDir.toString()), 162 "WAL backup directory should be empty after force delete"); 163 164 // Step 7: Take new full backup with continuous backup enabled 165 String backupIdContinuous = fullTableBackupWithContinuous(Lists.newArrayList(table1)); 166 167 // Step 8: Verify CONTINUOUS_BACKUP_REPLICATION_PEER is enabled again 168 assertTrue(continuousBackupReplicationPeerExistsAndEnabled(), 169 "Backup replication peer should be re-enabled after new backup"); 170 171 // And system table has new entry 172 Set<TableName> newTables = backupSystemTable.getContinuousBackupTableSet().keySet(); 173 assertTrue(newTables.contains(table1), 174 "System table should contain the table after new backup"); 175 176 // Cleanup 177 deleteBackup(backupIdContinuous); 178 } 179 180 private void setupBackupFolders(long currentTime) throws IOException { 181 setupBackupFolders(fs, backupWalDir, currentTime); 182 } 183 184 public static void setupBackupFolders(FileSystem fs, Path backupWalDir, long currentTime) 185 throws IOException { 186 Path walsDir = new Path(backupWalDir, WALS_DIR); 187 Path bulkLoadDir = new Path(backupWalDir, BULKLOAD_FILES_DIR); 188 189 fs.mkdirs(walsDir); 190 fs.mkdirs(bulkLoadDir); 191 192 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); 193 194 for (int i = 0; i < 5; i++) { 195 String dateStr = dateFormat.format(new Date(currentTime - (i * ONE_DAY_IN_MILLISECONDS))); 196 fs.mkdirs(new Path(walsDir, dateStr)); 197 fs.mkdirs(new Path(bulkLoadDir, dateStr)); 198 } 199 } 200 201 private static void verifyBackupCleanup(FileSystem fs, Path backupWalDir, long currentTime) 202 throws IOException { 203 Path walsDir = new Path(backupWalDir, WALS_DIR); 204 Path bulkLoadDir = new Path(backupWalDir, BULKLOAD_FILES_DIR); 205 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); 206 207 // Expect folders older than 3 days to be deleted 208 for (int i = 3; i < 5; i++) { 209 String oldDateStr = dateFormat.format(new Date(currentTime - (i * ONE_DAY_IN_MILLISECONDS))); 210 Path walPath = new Path(walsDir, oldDateStr); 211 Path bulkLoadPath = new Path(bulkLoadDir, oldDateStr); 212 assertFalse(fs.exists(walPath), 213 "Old WAL directory (" + walPath + ") should be deleted, but it exists!"); 214 assertFalse(fs.exists(bulkLoadPath), 215 "Old BulkLoad directory (" + bulkLoadPath + ") should be deleted, but it exists!"); 216 } 217 218 // Expect folders within the last 3 days to exist 219 for (int i = 0; i < 3; i++) { 220 String recentDateStr = 221 dateFormat.format(new Date(currentTime - (i * ONE_DAY_IN_MILLISECONDS))); 222 Path walPath = new Path(walsDir, recentDateStr); 223 Path bulkLoadPath = new Path(bulkLoadDir, recentDateStr); 224 225 assertTrue(fs.exists(walPath), 226 "Recent WAL directory (" + walPath + ") should exist, but it is missing!"); 227 assertTrue(fs.exists(bulkLoadPath), 228 "Recent BulkLoad directory (" + bulkLoadPath + ") should exist, but it is missing!"); 229 } 230 } 231 232 private void verifySystemTableUpdate(BackupSystemTable backupSystemTable, long currentTime) 233 throws IOException { 234 Map<TableName, Long> updatedTables = backupSystemTable.getContinuousBackupTableSet(); 235 236 for (Map.Entry<TableName, Long> entry : updatedTables.entrySet()) { 237 long updatedStartTime = entry.getValue(); 238 239 // Ensure that the updated start time is not earlier than the expected cutoff time 240 assertTrue(updatedStartTime >= (currentTime - (3 * ONE_DAY_IN_MILLISECONDS)), 241 "System table update failed!"); 242 } 243 } 244 245 public static void logDirectoryStructure(FileSystem fs, Path dir, String message) 246 throws IOException { 247 System.out.println(message); 248 listDirectory(fs, dir, " "); 249 } 250 251 public static void listDirectory(FileSystem fs, Path dir, String indent) throws IOException { 252 if (!fs.exists(dir)) { 253 System.out.println(indent + "[Missing] " + dir); 254 return; 255 } 256 FileStatus[] files = fs.listStatus(dir); 257 System.out.println(indent + dir); 258 for (FileStatus file : files) { 259 if (file.isDirectory()) { 260 listDirectory(fs, file.getPath(), indent + " "); 261 } else { 262 System.out.println(indent + " " + file.getPath()); 263 } 264 } 265 } 266 267 private boolean continuousBackupReplicationPeerExistsAndEnabled() throws IOException { 268 return TEST_UTIL.getAdmin().listReplicationPeers().stream().anyMatch( 269 peer -> peer.getPeerId().equals(CONTINUOUS_BACKUP_REPLICATION_PEER) && peer.isEnabled()); 270 } 271 272 private static boolean areWalAndBulkloadDirsEmpty(Configuration conf, String backupWalDir) 273 throws IOException { 274 BackupFileSystemManager manager = 275 new BackupFileSystemManager(CONTINUOUS_BACKUP_REPLICATION_PEER, conf, backupWalDir); 276 277 FileSystem fs = manager.getBackupFs(); 278 Path walDir = manager.getWalsDir(); 279 Path bulkloadDir = manager.getBulkLoadFilesDir(); 280 281 return isDirectoryEmpty(fs, walDir) && isDirectoryEmpty(fs, bulkloadDir); 282 } 283 284 private static boolean isDirectoryEmpty(FileSystem fs, Path dirPath) throws IOException { 285 if (!fs.exists(dirPath)) { 286 // Directory doesn't exist — treat as empty 287 return true; 288 } 289 FileStatus[] entries = fs.listStatus(dirPath); 290 return entries == null || entries.length == 0; 291 } 292 293 private static void deleteBackup(String backupId) throws Exception { 294 int ret = 295 ToolRunner.run(conf1, new BackupDriver(), new String[] { "delete", "-l", backupId, "-fd" }); 296 assertEquals(0, ret); 297 } 298 299 private String fullTableBackupWithContinuous(List<TableName> tables) throws IOException { 300 try (BackupAdmin admin = new BackupAdminImpl(TEST_UTIL.getConnection())) { 301 BackupRequest request = 302 createBackupRequest(BackupType.FULL, new ArrayList<>(tables), BACKUP_ROOT_DIR, false, true); 303 return admin.backupTables(request); 304 } 305 } 306 307}