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.junit.jupiter.api.Assertions.assertAll;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertNotNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Map;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileSystem;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.hbase.HBaseTestingUtil;
032import org.apache.hadoop.hbase.ServerName;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl;
035import org.apache.hadoop.hbase.backup.impl.IncrementalBackupManager;
036import org.apache.hadoop.hbase.backup.util.BackupUtils;
037import org.apache.hadoop.hbase.client.Connection;
038import org.apache.hadoop.hbase.client.ConnectionFactory;
039import org.apache.hadoop.hbase.regionserver.HRegionServer;
040import org.apache.hadoop.hbase.testclassification.LargeTests;
041import org.apache.hadoop.hbase.util.CommonFSUtils;
042import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
043import org.apache.hadoop.hbase.util.JVMClusterUtil;
044import org.apache.hadoop.hbase.wal.AbstractFSWALProvider;
045import org.junit.jupiter.api.BeforeAll;
046import org.junit.jupiter.api.Tag;
047import org.junit.jupiter.api.Test;
048
049@Tag(LargeTests.TAG)
050public class TestIncrementalBackupManager extends TestBackupBase {
051
052  @BeforeAll
053  public static void setUp() throws Exception {
054    TEST_UTIL = new HBaseTestingUtil();
055    conf1 = TEST_UTIL.getConfiguration();
056    conf1.setBoolean(AbstractFSWALProvider.SEPARATE_OLDLOGDIR, true);
057    autoRestoreOnFailure = true;
058    useSecondCluster = false;
059    setUpHelper();
060  }
061
062  @Test
063  public void testCollectWALFilesFromRegionServerDirectories() throws Exception {
064    testCollectWALFiles(true);
065  }
066
067  @Test
068  public void testCollectWALFilesFromFlatOldWALDirectory() throws Exception {
069    testCollectWALFiles(false);
070  }
071
072  private void testCollectWALFiles(boolean separateOldLogDir) throws Exception {
073    Configuration testConf = new Configuration(conf1);
074    testConf.setBoolean(AbstractFSWALProvider.SEPARATE_OLDLOGDIR, separateOldLogDir);
075    List<TableName> tables = List.of(table1);
076    try (Connection conn = ConnectionFactory.createConnection(testConf);
077      BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
078      String fullBackupId =
079        backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR));
080      assertTrue(checkSucceeded(fullBackupId));
081
082      try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, testConf)) {
083        BackupInfo backupInfo = manager.createBackupInfo("backup_test", BackupType.INCREMENTAL,
084          tables, BACKUP_ROOT_DIR, -1, -1, false, false);
085        Map<String, Long> previousTimestamps =
086          BackupUtils.getRSLogTimestampMins(manager.readLogTimestampMap());
087        ServerName serverName = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0).getServerName();
088        Long previousTimestamp = previousTimestamps.get(serverName.getAddress().toString());
089        assertNotNull(previousTimestamp);
090
091        TEST_UTIL.waitFor(30_000,
092          () -> EnvironmentEdgeManager.currentTime() > previousTimestamp + 1);
093        Path walRootDir = CommonFSUtils.getWALRootDir(conf1);
094        Path archiveDir = new Path(walRootDir,
095          AbstractFSWALProvider.getWALArchiveDirectoryName(testConf, serverName.toString()));
096        String walName = (separateOldLogDir ? "wal" : serverName.toString())
097          + BackupUtils.LOGNAME_SEPARATOR + (previousTimestamp + 1);
098        Path archivedWAL = new Path(archiveDir, walName);
099        FileSystem fs = walRootDir.getFileSystem(conf1);
100        fs.mkdirs(archiveDir);
101        fs.create(archivedWAL).close();
102
103        manager.getIncrBackupLogFileMap();
104
105        assertTrue(backupInfo.getIncrBackupFileList().contains(archivedWAL.toString()));
106      }
107    }
108  }
109
110  /**
111   * WALs can be archived out of order, so a region server that took part in the log roll can have
112   * an archived WAL newer than its roll result while an older WAL is still in the WALs directory.
113   * The newer archived WAL must be deferred to a later backup instead of being backed up twice, and
114   * the older WAL must still end up in a backup.
115   */
116  @Test
117  public void testOutOfOrderArchivedWALDoesNotSkipOlderWAL() throws Exception {
118    List<TableName> tables = List.of(table1);
119    HRegionServer rs = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
120    ServerName serverName = rs.getServerName();
121    Path walRootDir = CommonFSUtils.getWALRootDir(conf1);
122    FileSystem fs = walRootDir.getFileSystem(conf1);
123
124    try (Connection conn = ConnectionFactory.createConnection(conf1);
125      BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
126      String fullBackupId =
127        backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR));
128      assertTrue(checkSucceeded(fullBackupId));
129
130      long olderWALTs = EnvironmentEdgeManager.currentTime() + 1;
131      long newerWALTs = olderWALTs + 1;
132      Path walDir =
133        new Path(walRootDir, AbstractFSWALProvider.getWALDirectoryName(serverName.toString()));
134      Path olderWAL =
135        new Path(walDir, serverName.toString() + BackupUtils.LOGNAME_SEPARATOR + olderWALTs);
136      Path archiveDir = new Path(walRootDir,
137        AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, serverName.toString()));
138      Path newerArchivedWAL =
139        new Path(archiveDir, "wal" + BackupUtils.LOGNAME_SEPARATOR + newerWALTs);
140      fs.create(olderWAL).close();
141      fs.mkdirs(archiveDir);
142      fs.create(newerArchivedWAL).close();
143
144      try {
145        List<String> firstBackupFiles;
146        try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, conf1)) {
147          BackupInfo backupInfo = manager.createBackupInfo("backup_incr_1", BackupType.INCREMENTAL,
148            tables, BACKUP_ROOT_DIR, -1, -1, false, false);
149          Map<String, Long> boundaries = manager.getIncrBackupLogFileMap();
150          manager.writeRegionServerLogTimestamp(backupInfo.getTables(), boundaries);
151          firstBackupFiles = backupInfo.getIncrBackupFileList();
152        }
153        assertFalse(firstBackupFiles.contains(newerArchivedWAL.toString()),
154          "Archived WAL newer than the roll result should be deferred to a later backup: "
155            + firstBackupFiles);
156
157        TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > newerWALTs);
158        rs.getWalRoller().requestRollAll();
159        rs.getWalRoller().waitUntilWalRollFinished();
160
161        List<String> secondBackupFiles;
162        try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, conf1)) {
163          BackupInfo backupInfo = manager.createBackupInfo("backup_incr_2", BackupType.INCREMENTAL,
164            tables, BACKUP_ROOT_DIR, -1, -1, false, false);
165          manager.getIncrBackupLogFileMap();
166          secondBackupFiles = backupInfo.getIncrBackupFileList();
167        }
168
169        assertTrue(
170          firstBackupFiles.contains(olderWAL.toString())
171            || secondBackupFiles.contains(olderWAL.toString()),
172          "WAL " + olderWAL + " was not included in any backup. First backup: " + firstBackupFiles
173            + ", second backup: " + secondBackupFiles);
174      } finally {
175        fs.delete(olderWAL, false);
176        fs.delete(newerArchivedWAL, false);
177      }
178    }
179  }
180
181  /**
182   * A dead region server's WALs stay in its -splitting directory until WAL splitting finishes. An
183   * incremental backup that runs during the split must not lose them once they are archived.
184   */
185  @Test
186  public void testWALOfDeadServerStillSplittingIsBackedUpAfterArchiving() throws Exception {
187    List<TableName> tables = List.of(table1);
188    ServerName deadServer = ServerName.valueOf("deadhost", 16020, 1001L);
189    Path walRootDir = CommonFSUtils.getWALRootDir(conf1);
190    FileSystem fs = walRootDir.getFileSystem(conf1);
191    Path splittingDir = splittingDir(walRootDir, deadServer);
192    Path archiveDir = new Path(walRootDir,
193      AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, deadServer.toString()));
194
195    try (Connection conn = ConnectionFactory.createConnection(conf1);
196      BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
197      String fullBackupId =
198        backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR));
199      assertTrue(checkSucceeded(fullBackupId));
200
201      long walTs = EnvironmentEdgeManager.currentTime() + 1;
202      Path splittingWAL = new Path(splittingDir, walName(deadServer, walTs));
203      Path archivedWAL = archivedWAL(archiveDir, walTs);
204      fs.create(splittingWAL).close();
205
206      try {
207        // Make the live servers' roll results, and so the next boundaries, newer than the WAL.
208        TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > walTs);
209        rollAllLiveRegionServers();
210
211        List<String> firstBackupFiles = runIncrementalBackup(conn, tables, "backup_split_1");
212
213        fs.mkdirs(archiveDir);
214        assertTrue(fs.rename(splittingWAL, archivedWAL));
215        fs.delete(splittingDir, true);
216
217        List<String> secondBackupFiles = runIncrementalBackup(conn, tables, "backup_split_2");
218
219        assertTrue(
220          firstBackupFiles.contains(splittingWAL.toString())
221            || secondBackupFiles.contains(archivedWAL.toString()),
222          "WAL of the dead server was not included in any backup. First backup: " + firstBackupFiles
223            + ", second backup: " + secondBackupFiles);
224      } finally {
225        fs.delete(splittingDir, true);
226        fs.delete(archivedWAL, false);
227      }
228    }
229  }
230
231  /**
232   * WAL splitting archives a dead region server's WALs in parallel, so a newer WAL can be archived
233   * while older ones are still in the -splitting directory. Backing up the newer WAL must not move
234   * the boundary past the older ones.
235   */
236  @Test
237  public void testOlderWALsOfDeadServerAreNotSkippedWhenNewerWALIsArchivedFirst() throws Exception {
238    List<TableName> tables = List.of(table1);
239    ServerName deadServer = ServerName.valueOf("deadhost", 16020, 2002L);
240    Path walRootDir = CommonFSUtils.getWALRootDir(conf1);
241    FileSystem fs = walRootDir.getFileSystem(conf1);
242    Path splittingDir = splittingDir(walRootDir, deadServer);
243    Path archiveDir = new Path(walRootDir,
244      AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, deadServer.toString()));
245
246    try (Connection conn = ConnectionFactory.createConnection(conf1);
247      BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
248      String fullBackupId =
249        backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR));
250      assertTrue(checkSucceeded(fullBackupId));
251
252      long oldestTs = EnvironmentEdgeManager.currentTime() + 1;
253      long middleTs = oldestTs + 1;
254      long newestTs = oldestTs + 2;
255      Path oldestSplittingWAL = new Path(splittingDir, walName(deadServer, oldestTs));
256      Path middleSplittingWAL = new Path(splittingDir, walName(deadServer, middleTs));
257      Path oldestArchivedWAL = archivedWAL(archiveDir, oldestTs);
258      Path middleArchivedWAL = archivedWAL(archiveDir, middleTs);
259      Path newestArchivedWAL = archivedWAL(archiveDir, newestTs);
260      fs.create(oldestSplittingWAL).close();
261      fs.create(middleSplittingWAL).close();
262      fs.mkdirs(archiveDir);
263      fs.create(newestArchivedWAL).close();
264
265      try {
266        TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > newestTs);
267
268        List<String> firstBackupFiles = runIncrementalBackup(conn, tables, "backup_order_1");
269
270        assertTrue(fs.rename(oldestSplittingWAL, oldestArchivedWAL));
271        assertTrue(fs.rename(middleSplittingWAL, middleArchivedWAL));
272        fs.delete(splittingDir, true);
273
274        List<String> secondBackupFiles = runIncrementalBackup(conn, tables, "backup_order_2");
275
276        assertAll(
277          () -> assertTrue(
278            firstBackupFiles.contains(oldestSplittingWAL.toString())
279              || secondBackupFiles.contains(oldestArchivedWAL.toString()),
280            "Oldest WAL of the dead server was not included in any backup. First backup: "
281              + firstBackupFiles + ", second backup: " + secondBackupFiles),
282          () -> assertTrue(
283            firstBackupFiles.contains(middleSplittingWAL.toString())
284              || secondBackupFiles.contains(middleArchivedWAL.toString()),
285            "Middle WAL of the dead server was not included in any backup. First backup: "
286              + firstBackupFiles + ", second backup: " + secondBackupFiles));
287      } finally {
288        fs.delete(splittingDir, true);
289        fs.delete(oldestArchivedWAL, false);
290        fs.delete(middleArchivedWAL, false);
291        fs.delete(newestArchivedWAL, false);
292      }
293    }
294  }
295
296  /**
297   * A dead region server can keep an old WAL, already covered by its boundary, in its -splitting
298   * directory across several incremental backups. Its boundary must survive those backups, or a
299   * later backup would include that WAL again and could bring back deleted data.
300   */
301  @Test
302  public void testDeadServerKeepsBoundaryWhileOldWALIsStillSplitting() throws Exception {
303    List<TableName> tables = List.of(table1);
304    ServerName deadServer = ServerName.valueOf("deadhost", 16020, 3003L);
305    Path walRootDir = CommonFSUtils.getWALRootDir(conf1);
306    FileSystem fs = walRootDir.getFileSystem(conf1);
307    Path splittingDir = splittingDir(walRootDir, deadServer);
308    Path archiveDir = new Path(walRootDir,
309      AbstractFSWALProvider.getWALArchiveDirectoryName(conf1, deadServer.toString()));
310
311    try (Connection conn = ConnectionFactory.createConnection(conf1);
312      BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
313      String fullBackupId =
314        backupAdmin.backupTables(createBackupRequest(BackupType.FULL, tables, BACKUP_ROOT_DIR));
315      assertTrue(checkSucceeded(fullBackupId));
316
317      long stuckTs = EnvironmentEdgeManager.currentTime() + 1;
318      long archivedTs = stuckTs + 1;
319      Path archivedWAL = archivedWAL(archiveDir, archivedTs);
320      Path stuckSplittingWAL = new Path(splittingDir, walName(deadServer, stuckTs));
321      Path stuckArchivedWAL = archivedWAL(archiveDir, stuckTs);
322      fs.mkdirs(archiveDir);
323      fs.create(archivedWAL).close();
324
325      try {
326        TEST_UTIL.waitFor(30_000, () -> EnvironmentEdgeManager.currentTime() > archivedTs);
327        List<String> firstBackupFiles = runIncrementalBackup(conn, tables, "backup_stuck_1");
328        assertTrue(firstBackupFiles.contains(archivedWAL.toString()),
329          "Archived WAL of the dead server should be in the first backup: " + firstBackupFiles);
330
331        fs.create(stuckSplittingWAL).close();
332        List<String> laterBackupFiles = new ArrayList<>();
333        laterBackupFiles.addAll(runIncrementalBackup(conn, tables, "backup_stuck_2"));
334        laterBackupFiles.addAll(runIncrementalBackup(conn, tables, "backup_stuck_3"));
335
336        assertTrue(fs.rename(stuckSplittingWAL, stuckArchivedWAL));
337        fs.delete(splittingDir, true);
338        laterBackupFiles.addAll(runIncrementalBackup(conn, tables, "backup_stuck_4"));
339
340        assertFalse(
341          laterBackupFiles.contains(stuckSplittingWAL.toString())
342            || laterBackupFiles.contains(stuckArchivedWAL.toString()),
343          "WAL already covered by the dead server's boundary was backed up again: "
344            + laterBackupFiles);
345      } finally {
346        fs.delete(splittingDir, true);
347        fs.delete(archivedWAL, false);
348        fs.delete(stuckArchivedWAL, false);
349      }
350    }
351  }
352
353  private static Path splittingDir(Path walRootDir, ServerName serverName) {
354    return new Path(walRootDir, AbstractFSWALProvider.getWALDirectoryName(serverName.toString())
355      + AbstractFSWALProvider.SPLITTING_EXT);
356  }
357
358  private static Path archivedWAL(Path archiveDir, long ts) {
359    return new Path(archiveDir, "wal" + BackupUtils.LOGNAME_SEPARATOR + ts);
360  }
361
362  private static String walName(ServerName serverName, long ts) {
363    return serverName.toString() + BackupUtils.LOGNAME_SEPARATOR + ts;
364  }
365
366  private static void rollAllLiveRegionServers() throws Exception {
367    for (JVMClusterUtil.RegionServerThread rst : TEST_UTIL.getMiniHBaseCluster()
368      .getLiveRegionServerThreads()) {
369      rst.getRegionServer().getWalRoller().requestRollAll();
370      rst.getRegionServer().getWalRoller().waitUntilWalRollFinished();
371    }
372  }
373
374  private static List<String> runIncrementalBackup(Connection conn, List<TableName> tables,
375    String backupId) throws Exception {
376    try (IncrementalBackupManager manager = new IncrementalBackupManager(conn, conf1)) {
377      BackupInfo backupInfo = manager.createBackupInfo(backupId, BackupType.INCREMENTAL, tables,
378        BACKUP_ROOT_DIR, -1, -1, false, false);
379      Map<String, Long> boundaries = manager.getIncrBackupLogFileMap();
380      manager.writeRegionServerLogTimestamp(backupInfo.getTables(), boundaries);
381      return backupInfo.getIncrBackupFileList();
382    }
383  }
384}