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}