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.master; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertFalse; 022import static org.junit.Assert.assertTrue; 023 024import java.util.Collection; 025import java.util.Collections; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import org.apache.hadoop.fs.FileStatus; 031import org.apache.hadoop.fs.Path; 032import org.apache.hadoop.hbase.HBaseClassTestRule; 033import org.apache.hadoop.hbase.TableName; 034import org.apache.hadoop.hbase.backup.BackupType; 035import org.apache.hadoop.hbase.backup.TestBackupBase; 036import org.apache.hadoop.hbase.backup.impl.BackupSystemTable; 037import org.apache.hadoop.hbase.client.Connection; 038import org.apache.hadoop.hbase.client.Put; 039import org.apache.hadoop.hbase.client.Table; 040import org.apache.hadoop.hbase.master.HMaster; 041import org.apache.hadoop.hbase.testclassification.LargeTests; 042import org.apache.hadoop.hbase.util.Bytes; 043import org.junit.ClassRule; 044import org.junit.Test; 045import org.junit.experimental.categories.Category; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049@Category(LargeTests.class) 050public class TestBackupLogCleaner extends TestBackupBase { 051 052 @ClassRule 053 public static final HBaseClassTestRule CLASS_RULE = 054 HBaseClassTestRule.forClass(TestBackupLogCleaner.class); 055 056 private static final Logger LOG = LoggerFactory.getLogger(TestBackupLogCleaner.class); 057 058 // implements all test cases in 1 test since incremental full backup/ 059 // incremental backup has dependencies 060 061 @Test 062 public void testBackupLogCleaner() throws Exception { 063 Path backupRoot1 = new Path(BACKUP_ROOT_DIR, "root1"); 064 Path backupRoot2 = new Path(BACKUP_ROOT_DIR, "root2"); 065 066 List<TableName> tableSetFull = List.of(table1, table2, table3, table4); 067 List<TableName> tableSet14 = List.of(table1, table4); 068 List<TableName> tableSet23 = List.of(table2, table3); 069 070 try (BackupSystemTable systemTable = new BackupSystemTable(TEST_UTIL.getConnection())) { 071 // Verify that we have no backup sessions yet 072 assertFalse(systemTable.hasBackupSessions()); 073 074 BackupLogCleaner cleaner = new BackupLogCleaner(); 075 cleaner.setConf(TEST_UTIL.getConfiguration()); 076 cleaner.init(Map.of(HMaster.MASTER, TEST_UTIL.getHBaseCluster().getMaster())); 077 078 // All WAL files can be deleted because we do not have backups 079 List<FileStatus> walFilesBeforeBackup = getListOfWALFiles(TEST_UTIL.getConfiguration()); 080 Iterable<FileStatus> deletable = cleaner.getDeletableFiles(walFilesBeforeBackup); 081 assertEquals(walFilesBeforeBackup, deletable); 082 083 // Create a FULL backup B1 in backupRoot R1, containing all tables 084 String backupIdB1 = backupTables(BackupType.FULL, tableSetFull, backupRoot1.toString()); 085 assertTrue(checkSucceeded(backupIdB1)); 086 087 // As part of a backup, WALs are rolled, so we expect a new WAL file 088 Set<FileStatus> walFilesAfterB1 = 089 mergeAsSet(walFilesBeforeBackup, getListOfWALFiles(TEST_UTIL.getConfiguration())); 090 assertTrue(walFilesBeforeBackup.size() < walFilesAfterB1.size()); 091 092 // Currently, we only have backup B1, so we can delete any WAL preceding B1 093 deletable = cleaner.getDeletableFiles(walFilesAfterB1); 094 assertEquals(toSet(walFilesBeforeBackup), toSet(deletable)); 095 096 // Insert some data 097 Connection conn = TEST_UTIL.getConnection(); 098 try (Table t1 = conn.getTable(table1)) { 099 Put p1; 100 for (int i = 0; i < NB_ROWS_IN_BATCH; i++) { 101 p1 = new Put(Bytes.toBytes("row-t1" + i)); 102 p1.addColumn(famName, qualName, Bytes.toBytes("val" + i)); 103 t1.put(p1); 104 } 105 } 106 107 try (Table t2 = conn.getTable(table2)) { 108 Put p2; 109 for (int i = 0; i < 5; i++) { 110 p2 = new Put(Bytes.toBytes("row-t2" + i)); 111 p2.addColumn(famName, qualName, Bytes.toBytes("val" + i)); 112 t2.put(p2); 113 } 114 } 115 116 // Create an INCREMENTAL backup B2 in backupRoot R1, requesting tables 1 & 4. 117 // Note that incremental tables always include all tables already included in the backup root, 118 // i.e. the backup will contain all tables (1, 2, 3, 4), ignoring what we specify here. 119 LOG.debug("Creating B2"); 120 String backupIdB2 = backupTables(BackupType.INCREMENTAL, tableSet14, backupRoot1.toString()); 121 assertTrue(checkSucceeded(backupIdB2)); 122 123 // As part of a backup, WALs are rolled, so we expect a new WAL file 124 Set<FileStatus> walFilesAfterB2 = 125 mergeAsSet(walFilesAfterB1, getListOfWALFiles(TEST_UTIL.getConfiguration())); 126 assertTrue(walFilesAfterB1.size() < walFilesAfterB2.size()); 127 128 // At this point, we have backups in root R1: B1 and B2. 129 // We only consider the most recent backup (B2) to determine which WALs can be deleted: 130 // all WALs preceding B2 131 deletable = cleaner.getDeletableFiles(walFilesAfterB2); 132 assertEquals(toSet(walFilesAfterB1), toSet(deletable)); 133 134 // Create a FULL backup B3 in backupRoot R2, containing tables 1 & 4 135 LOG.debug("Creating B3"); 136 String backupIdB3 = backupTables(BackupType.FULL, tableSetFull, backupRoot2.toString()); 137 assertTrue(checkSucceeded(backupIdB3)); 138 139 // As part of a backup, WALs are rolled, so we expect a new WAL file 140 Set<FileStatus> walFilesAfterB3 = 141 mergeAsSet(walFilesAfterB2, getListOfWALFiles(TEST_UTIL.getConfiguration())); 142 assertTrue(walFilesAfterB2.size() < walFilesAfterB3.size()); 143 144 // At this point, we have backups in: 145 // root R1: B1 (timestamp=0, all tables), B2 (TS=1, all tables) 146 // root R2: B3 (TS=2, [T1, T4]) 147 // 148 // To determine the WAL-deletion boundary, we only consider the most recent backup per root, 149 // so [B2, B3]. From these, we take the least recent as WAL-deletion boundary: B2, it contains 150 // all tables, so acts as the deletion boundary. I.e. only WALs preceding B2 are deletable. 151 deletable = cleaner.getDeletableFiles(walFilesAfterB3); 152 assertEquals(toSet(walFilesAfterB1), toSet(deletable)); 153 154 // Create a FULL backup B4 in backupRoot R1, with a subset of tables 155 LOG.debug("Creating B4"); 156 String backupIdB4 = backupTables(BackupType.FULL, tableSet14, backupRoot1.toString()); 157 assertTrue(checkSucceeded(backupIdB4)); 158 159 // As part of a backup, WALs are rolled, so we expect a new WAL file 160 Set<FileStatus> walFilesAfterB4 = 161 mergeAsSet(walFilesAfterB3, getListOfWALFiles(TEST_UTIL.getConfiguration())); 162 assertTrue(walFilesAfterB3.size() < walFilesAfterB4.size()); 163 164 // At this point, we have backups in: 165 // root R1: B1 (timestamp=0, all tables), B2 (TS=1, all tables), B4 (TS=3, [T1, T4]) 166 // root R2: B3 (TS=2, [T1, T4]) 167 // 168 // To determine the WAL-deletion boundary, we only consider the most recent backup per root, 169 // so [B4, B3]. They contain the following timestamp boundaries per table: 170 // B4: { T1: 3, T2: 1, T3: 1, T4: 3 } 171 // B3: { T1: 2, T4: 2 } 172 // Taking the minimum timestamp (= 1), this means all WALs preceding B2 can be deleted. 173 deletable = cleaner.getDeletableFiles(walFilesAfterB4); 174 assertEquals(toSet(walFilesAfterB1), toSet(deletable)); 175 176 // Create a FULL backup B5 in backupRoot R1, for tables 2 & 3 177 String backupIdB5 = backupTables(BackupType.FULL, tableSet23, backupRoot1.toString()); 178 assertTrue(checkSucceeded(backupIdB5)); 179 180 // As part of a backup, WALs are rolled, so we expect a new WAL file 181 Set<FileStatus> walFilesAfterB5 = 182 mergeAsSet(walFilesAfterB4, getListOfWALFiles(TEST_UTIL.getConfiguration())); 183 assertTrue(walFilesAfterB4.size() < walFilesAfterB5.size()); 184 185 // At this point, we have backups in: 186 // root R1: ..., B2 (TS=1, all tables), B4 (TS=3, [T1, T4]), B5 (TS=4, [T2, T3]) 187 // root R2: B3 (TS=2, [T1, T4]) 188 // 189 // To determine the WAL-deletion boundary, we only consider the most recent backup per root, 190 // so [B5, B3]. They contain the following timestamp boundaries per table: 191 // B4: { T1: 3, T2: 4, T3: 4, T4: 3 } 192 // B3: { T1: 2, T4: 2 } 193 // Taking the minimum timestamp (= 2), this means all WALs preceding B3 can be deleted. 194 deletable = cleaner.getDeletableFiles(walFilesAfterB5); 195 assertEquals(toSet(walFilesAfterB2), toSet(deletable)); 196 } 197 } 198 199 private Set<FileStatus> mergeAsSet(Collection<FileStatus> toCopy, Collection<FileStatus> toAdd) { 200 Set<FileStatus> result = new LinkedHashSet<>(toCopy); 201 result.addAll(toAdd); 202 return result; 203 } 204 205 private <T> Set<T> toSet(Iterable<T> iterable) { 206 Set<T> result = new LinkedHashSet<>(); 207 iterable.forEach(result::add); 208 return result; 209 } 210 211 @Test 212 public void testCleansUpHMasterWal() { 213 Path path = new Path("/hbase/MasterData/WALs/hmaster,60000,1718808578163"); 214 assertTrue(BackupLogCleaner.canDeleteFile(Collections.emptyMap(), path)); 215 } 216 217 @Test 218 public void testCleansUpArchivedHMasterWal() { 219 Path normalPath = 220 new Path("/hbase/oldWALs/hmaster%2C60000%2C1716224062663.1716247552189$masterlocalwal$"); 221 assertTrue(BackupLogCleaner.canDeleteFile(Collections.emptyMap(), normalPath)); 222 223 Path masterPath = new Path( 224 "/hbase/MasterData/oldWALs/hmaster%2C60000%2C1716224062663.1716247552189$masterlocalwal$"); 225 assertTrue(BackupLogCleaner.canDeleteFile(Collections.emptyMap(), masterPath)); 226 } 227}