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.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertFalse;
023import static org.junit.jupiter.api.Assertions.assertNotNull;
024import static org.junit.jupiter.api.Assertions.assertNull;
025import static org.junit.jupiter.api.Assertions.assertTrue;
026
027import java.io.IOException;
028import java.util.ArrayList;
029import java.util.List;
030import java.util.Map;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.hbase.HBaseTestingUtil;
033import org.apache.hadoop.hbase.ServerName;
034import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
035import org.apache.hadoop.hbase.TableName;
036import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl;
037import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
038import org.apache.hadoop.hbase.backup.impl.FullTableBackupClient;
039import org.apache.hadoop.hbase.backup.impl.TableBackupClient;
040import org.apache.hadoop.hbase.backup.util.BackupUtils;
041import org.apache.hadoop.hbase.client.Admin;
042import org.apache.hadoop.hbase.client.Connection;
043import org.apache.hadoop.hbase.client.ConnectionFactory;
044import org.apache.hadoop.hbase.client.Delete;
045import org.apache.hadoop.hbase.client.Get;
046import org.apache.hadoop.hbase.client.Put;
047import org.apache.hadoop.hbase.client.RegionInfo;
048import org.apache.hadoop.hbase.client.ResultScanner;
049import org.apache.hadoop.hbase.client.Scan;
050import org.apache.hadoop.hbase.client.Table;
051import org.apache.hadoop.hbase.master.HMaster;
052import org.apache.hadoop.hbase.master.procedure.ServerCrashProcedure;
053import org.apache.hadoop.hbase.regionserver.HRegion;
054import org.apache.hadoop.hbase.regionserver.HRegionServer;
055import org.apache.hadoop.hbase.testclassification.LargeTests;
056import org.apache.hadoop.hbase.util.Bytes;
057import org.junit.jupiter.api.BeforeAll;
058import org.junit.jupiter.api.Tag;
059import org.junit.jupiter.api.Test;
060import org.slf4j.Logger;
061import org.slf4j.LoggerFactory;
062
063import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
064
065/**
066 * Tests that WAL files from offline/inactive RegionServers are handled correctly during backup.
067 * Specifically verifies that WALs from an offline RS are:
068 * <ol>
069 * <li>Backed up once in the first backup after the RS goes offline</li>
070 * <li>NOT re-backed up in subsequent backups</li>
071 * </ol>
072 */
073@Tag(LargeTests.TAG)
074public class TestBackupOfflineRS extends TestBackupBase {
075
076  private static final Logger LOG = LoggerFactory.getLogger(TestBackupOfflineRS.class);
077
078  @BeforeAll
079  public static void setUp() throws Exception {
080    TEST_UTIL = new HBaseTestingUtil();
081    conf1 = TEST_UTIL.getConfiguration();
082    conf1.setInt("hbase.regionserver.info.port", -1);
083    conf1.setInt(HMaster.HBASE_MASTER_CLEANER_INTERVAL, Integer.MAX_VALUE);
084    autoRestoreOnFailure = true;
085    useSecondCluster = false;
086    setUpHelper();
087    TEST_UTIL.getMiniHBaseCluster().startRegionServer();
088    TEST_UTIL.waitTableAvailable(table1);
089  }
090
091  private static Runnable afterSnapshotHook;
092
093  /** Full backup client that runs {@link #afterSnapshotHook} once the table snapshots exist. */
094  public static class FullTableBackupClientWithHook extends FullTableBackupClient {
095    @Override
096    protected void snapshotCopy(BackupInfo backupInfo) throws IOException {
097      afterSnapshotHook.run();
098      super.snapshotCopy(backupInfo);
099    }
100  }
101
102  private static void stopRegionServerAndWait(ServerName serverName) throws Exception {
103    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
104    HMaster master = cluster.getMaster();
105    cluster.stopRegionServer(serverName);
106    cluster.waitForRegionServerToStop(serverName, 60_000);
107    TEST_UTIL.waitFor(60_000,
108      () -> master.getProcedures().stream().filter(ServerCrashProcedure.class::isInstance)
109        .map(ServerCrashProcedure.class::cast)
110        .anyMatch(scp -> scp.getServerName().equals(serverName) && scp.isFinished()));
111    TEST_UTIL.waitUntilNoRegionsInTransition(60_000);
112  }
113
114  private static void restore(String backupId, TableName sourceTable, TableName restoredTable)
115    throws Exception {
116    try (Connection conn = ConnectionFactory.createConnection(conf1);
117      BackupAdminImpl backupAdmin = new BackupAdminImpl(conn)) {
118      backupAdmin.restore(BackupUtils.createRestoreRequest(BACKUP_ROOT_DIR, backupId, false,
119        new TableName[] { sourceTable }, new TableName[] { restoredTable }, true));
120    }
121  }
122
123  private static void restoreAndAssertAllRowsPresent(String backupId, String restoredTableName,
124    String message) throws Exception {
125    TableName restoredTable = TableName.valueOf(restoredTableName);
126    restore(backupId, table1, restoredTable);
127    assertEquals(TEST_UTIL.countRows(table1), TEST_UTIL.countRows(restoredTable), message);
128  }
129
130  private static Long boundary(BackupSystemTable sysTable, String host) throws IOException {
131    return sysTable.readLogTimestampMap(BACKUP_ROOT_DIR).get(table1).get(host);
132  }
133
134  private static List<String> walsOfHost(BackupSystemTable sysTable, String backupId, String host)
135    throws IOException {
136    List<String> wals = sysTable.readBackupInfo(backupId).getIncrBackupFileList();
137    if (wals == null) {
138      return List.of();
139    }
140    return wals.stream()
141      .filter(wal -> host.equals(BackupUtils.parseHostNameFromLogFile(new Path(wal)))).toList();
142  }
143
144  private static void moveRegionAndWait(TableName table, HRegionServer destination)
145    throws Exception {
146    try (Admin admin = TEST_UTIL.getConnection().getAdmin()) {
147      RegionInfo region = admin.getRegions(table).get(0);
148      admin.move(region.getEncodedNameAsBytes(), destination.getServerName());
149    }
150    TEST_UTIL.waitFor(60_000, () -> !destination.getRegions(table).isEmpty());
151    TEST_UTIL.waitUntilAllRegionsAssigned(table);
152  }
153
154  /**
155   * Tests that when a full backup is taken while an RS is offline (with WALs in oldlogs), the
156   * offline host's timestamps are recorded so subsequent incremental backups don't reinclude those
157   * WALs.
158   */
159  @Test
160  public void testBackupWithOfflineRS() throws Exception {
161    LOG.info("Starting testBackupWithOfflineRS");
162
163    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
164    List<TableName> tables = Lists.newArrayList(table1);
165
166    HRegionServer rsBeforeStop = cluster.startRegionServerAndWait(10000).getRegionServer();
167    moveRegionAndWait(table1, rsBeforeStop);
168
169    LOG.info("Inserting data to generate WAL entries");
170    try (Connection conn = ConnectionFactory.createConnection(conf1)) {
171      insertIntoTable(conn, table1, famName, 2, 100);
172    }
173
174    String offlineHost =
175      rsBeforeStop.getServerName().getHostname() + ":" + rsBeforeStop.getServerName().getPort();
176    LOG.info("Stopping RS: {}", offlineHost);
177
178    stopRegionServerAndWait(rsBeforeStop.getServerName());
179
180    LOG.info("Taking full backup (with offline RS WALs in oldlogs)");
181    String fullBackupId = fullTableBackup(tables);
182    assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed");
183
184    try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
185      Map<TableName, Map<String, Long>> timestamps = sysTable.readLogTimestampMap(BACKUP_ROOT_DIR);
186      Map<String, Long> rsTimestamps = timestamps.get(table1);
187      LOG.info("RS timestamps after full backup: {}", rsTimestamps);
188
189      Long tsAfterFullBackup = rsTimestamps.get(offlineHost);
190      assertNotNull(tsAfterFullBackup,
191        "Offline host should have timestamp recorded in trslm after full backup");
192
193      LOG.info("Taking incremental backup (should NOT include offline RS WALs)");
194      String incrBackupId = incrementalTableBackup(tables);
195      assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed");
196
197      assertEquals(tsAfterFullBackup, boundary(sysTable, offlineHost),
198        "Incremental backup moved the boundary of the offline host");
199      List<String> offlineHostWals = walsOfHost(sysTable, incrBackupId, offlineHost);
200      assertTrue(offlineHostWals.isEmpty(),
201        "Incremental backup included WALs of the offline host that the full backup covers: "
202          + offlineHostWals);
203    }
204  }
205
206  /**
207   * Tests that WALs written to an RS after a full backup are correctly included in the subsequent
208   * incremental backup, even if that RS has gone offline before the incremental runs.
209   */
210  @Test
211  public void testRSGoesOfflineAfterFullBackupBeforeIncremental() throws Exception {
212    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
213    List<TableName> tables = Lists.newArrayList(table1);
214
215    HRegionServer rsBeforeStop = cluster.startRegionServerAndWait(10000).getRegionServer();
216
217    try (Connection conn = ConnectionFactory.createConnection(conf1)) {
218      insertIntoTable(conn, table1, famName, 3, 50);
219    }
220
221    String fullBackupId = fullTableBackup(tables);
222    assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed");
223
224    moveRegionAndWait(table1, rsBeforeStop);
225    try (Connection conn = ConnectionFactory.createConnection(conf1)) {
226      insertIntoTable(conn, table1, famName, 4, 50);
227    }
228
229    rsBeforeStop.getWalRoller().requestRollAll();
230    rsBeforeStop.getWalRoller().waitUntilWalRollFinished();
231    String offlineHost =
232      rsBeforeStop.getServerName().getHostname() + ":" + rsBeforeStop.getServerName().getPort();
233
234    stopRegionServerAndWait(rsBeforeStop.getServerName());
235
236    String incrBackupId = incrementalTableBackup(tables);
237    assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed");
238
239    restoreAndAssertAllRowsPresent(incrBackupId, "table1_rs_offline_after_full_backup",
240      "Restored table should contain the rows written to the RS that went offline");
241
242    try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
243      Long boundaryAfterIncr1 = boundary(sysTable, offlineHost);
244      assertNotNull(boundaryAfterIncr1,
245        "Offline RS should have a timestamp boundary after the incremental backed up its WALs");
246      assertFalse(walsOfHost(sysTable, incrBackupId, offlineHost).isEmpty(),
247        "First incremental after the RS went offline should include its WALs");
248      Long staleLogRoll =
249        sysTable.readRegionServerLastLogRollResult(BACKUP_ROOT_DIR).get(offlineHost);
250
251      String incrBackupId2 = incrementalTableBackup(tables);
252      assertTrue(checkSucceeded(incrBackupId2), "Second incremental backup should succeed");
253      Long boundaryAfterIncr2 = boundary(sysTable, offlineHost);
254
255      String incrBackupId3 = incrementalTableBackup(tables);
256      assertTrue(checkSucceeded(incrBackupId3), "Third incremental backup should succeed");
257      Long boundaryAfterIncr3 = boundary(sysTable, offlineHost);
258
259      String incrBackupId4 = incrementalTableBackup(tables);
260      assertTrue(checkSucceeded(incrBackupId4), "Fourth incremental backup should succeed");
261      Long boundaryAfterIncr4 = boundary(sysTable, offlineHost);
262
263      String message = "Incremental backup moved the boundary of the offline RS, which would back "
264        + "up its WALs again (last log roll = " + staleLogRoll + ")";
265      List<String> walsBackedUpAgain = new ArrayList<>();
266      for (String laterBackupId : List.of(incrBackupId2, incrBackupId3, incrBackupId4)) {
267        walsBackedUpAgain.addAll(walsOfHost(sysTable, laterBackupId, offlineHost));
268      }
269      assertAll(() -> assertEquals(boundaryAfterIncr1, boundaryAfterIncr2, message),
270        () -> assertEquals(boundaryAfterIncr1, boundaryAfterIncr3, message),
271        () -> assertEquals(boundaryAfterIncr1, boundaryAfterIncr4, message),
272        () -> assertTrue(walsBackedUpAgain.isEmpty(),
273          "Later incremental backups included WALs of the offline RS again: " + walsBackedUpAgain));
274    }
275  }
276
277  /**
278   * Tests that a brand-new RS that comes online and goes offline before any backup correctly has
279   * its WALs covered by the full backup.
280   */
281  @Test
282  public void testTransientRSBeforeFullBackup() throws Exception {
283    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
284    List<TableName> tables = Lists.newArrayList(table1);
285
286    HRegionServer transientRS = cluster.startRegionServerAndWait(10000).getRegionServer();
287    moveRegionAndWait(table1, transientRS);
288    try (Connection conn = ConnectionFactory.createConnection(conf1)) {
289      insertIntoTable(conn, table1, famName, 5, 50);
290    }
291    transientRS.getWalRoller().requestRollAll();
292    transientRS.getWalRoller().waitUntilWalRollFinished();
293    String transientHost =
294      transientRS.getServerName().getHostname() + ":" + transientRS.getServerName().getPort();
295
296    stopRegionServerAndWait(transientRS.getServerName());
297
298    String fullBackupId = fullTableBackup(tables);
299    assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed");
300
301    try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
302      Long boundaryAfterFullBackup = boundary(sysTable, transientHost);
303      assertNotNull(boundaryAfterFullBackup,
304        "Transient RS that went offline before full backup should have its WAL boundary recorded");
305
306      String incrBackupId = incrementalTableBackup(tables);
307      assertTrue(checkSucceeded(incrBackupId),
308        "Incremental backup after transient RS should succeed");
309
310      assertEquals(boundaryAfterFullBackup, boundary(sysTable, transientHost),
311        "Incremental backup moved the boundary of the transient RS");
312    }
313  }
314
315  /**
316   * Tests that WALs from an RS that comes online and goes offline between a full backup and an
317   * incremental backup are correctly included in the incremental backup and not re-included in
318   * subsequent incremental backups.
319   */
320  @Test
321  public void testTransientRSAfterFullBackupBeforeIncremental() throws Exception {
322    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
323    List<TableName> tables = Lists.newArrayList(table1);
324
325    try (Connection conn = ConnectionFactory.createConnection(conf1)) {
326      insertIntoTable(conn, table1, famName, 6, 50);
327    }
328
329    String fullBackupId = fullTableBackup(tables);
330    assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed");
331
332    HRegionServer transientRS = cluster.startRegionServerAndWait(10000).getRegionServer();
333    moveRegionAndWait(table1, transientRS);
334    try (Connection conn = ConnectionFactory.createConnection(conf1)) {
335      insertIntoTable(conn, table1, famName, 7, 50);
336    }
337    transientRS.getWalRoller().requestRollAll();
338    transientRS.getWalRoller().waitUntilWalRollFinished();
339    String transientHost =
340      transientRS.getServerName().getHostname() + ":" + transientRS.getServerName().getPort();
341
342    stopRegionServerAndWait(transientRS.getServerName());
343
344    String incrBackupId = incrementalTableBackup(tables);
345    assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed");
346
347    restoreAndAssertAllRowsPresent(incrBackupId, "table1_transient_rs_after_full_backup",
348      "Restored table should contain the rows written to the transient RS");
349
350    try (BackupSystemTable sysTable = new BackupSystemTable(TEST_UTIL.getConnection())) {
351      Long boundaryAfterIncr1 = boundary(sysTable, transientHost);
352      assertNotNull(boundaryAfterIncr1,
353        "Transient RS should have a timestamp boundary after the incremental backed up its WALs");
354
355      String incrBackupId2 = incrementalTableBackup(tables);
356      assertTrue(checkSucceeded(incrBackupId2), "Second incremental backup should succeed");
357
358      assertEquals(boundaryAfterIncr1, boundary(sysTable, transientHost),
359        "Second incremental backup moved the boundary of the transient RS");
360    }
361  }
362
363  /**
364   * Tests that edits written during a full backup, after its log roll and table snapshot, to an RS
365   * that started after that log roll, are included in the next incremental backup.
366   */
367  @Test
368  public void testRSStartedDuringFullBackup() throws Exception {
369    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
370    List<TableName> tables = Lists.newArrayList(table1);
371
372    afterSnapshotHook = () -> {
373      try {
374        HRegionServer newRS = cluster.startRegionServerAndWait(10000).getRegionServer();
375        moveRegionAndWait(table1, newRS);
376        try (Connection conn = ConnectionFactory.createConnection(conf1)) {
377          insertIntoTable(conn, table1, famName, 8, 50).close();
378        }
379        stopRegionServerAndWait(newRS.getServerName());
380      } catch (Exception e) {
381        throw new RuntimeException(e);
382      }
383    };
384    conf1.set(TableBackupClient.BACKUP_CLIENT_IMPL_CLASS,
385      FullTableBackupClientWithHook.class.getName());
386    String fullBackupId;
387    try {
388      fullBackupId = fullTableBackup(tables);
389    } finally {
390      conf1.unset(TableBackupClient.BACKUP_CLIENT_IMPL_CLASS);
391    }
392    assertTrue(checkSucceeded(fullBackupId), "Full backup should succeed");
393
394    String incrBackupId = incrementalTableBackup(tables);
395    assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed");
396
397    restoreAndAssertAllRowsPresent(incrBackupId, "table1_rs_started_during_full_backup",
398      "Restored table should contain all rows, including those written to the new RS");
399  }
400
401  /**
402   * Tests that a row deleted before a full backup is not brought back by the WALs of a region
403   * server that went offline between two backups. The row and its delete marker are compacted away
404   * before the full backup, so only the offline RS's old WAL, which still holds the put, could
405   * bring it back.
406   */
407  @Test
408  public void testDeletedRowIsNotResurrectedByOfflineRSWALs() throws Exception {
409    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
410    List<TableName> tables = Lists.newArrayList(table1);
411    byte[] deletedRow = Bytes.toBytes("row-deleted");
412
413    HRegionServer offlineRS = cluster.startRegionServerAndWait(10000).getRegionServer();
414    HRegionServer liveRS = cluster.startRegionServerAndWait(10000).getRegionServer();
415
416    try (Table table = TEST_UTIL.getConnection().getTable(table1)) {
417      moveRegionAndWait(table1, offlineRS);
418
419      String firstFullBackupId = fullTableBackup(tables);
420      assertTrue(checkSucceeded(firstFullBackupId), "First full backup should succeed");
421
422      for (int i = 0; i < 10; i++) {
423        table.put(new Put(Bytes.toBytes("row-kept-" + i)).addColumn(famName, qualName,
424          Bytes.toBytes("value")));
425      }
426      table.put(new Put(deletedRow).addColumn(famName, qualName, Bytes.toBytes("value")));
427      for (HRegion region : offlineRS.getRegions(table1)) {
428        region.flush(true);
429      }
430
431      moveRegionAndWait(table1, liveRS);
432
433      table.delete(new Delete(deletedRow));
434      for (HRegion region : liveRS.getRegions(table1)) {
435        region.flush(true);
436        region.compact(true);
437      }
438
439      assertTrue(table.get(new Get(deletedRow)).isEmpty(),
440        "Deleted row should be gone from the source table");
441      Scan rawScan = new Scan().withStartRow(deletedRow).withStopRow(deletedRow, true).setRaw(true);
442      try (ResultScanner scanner = table.getScanner(rawScan)) {
443        assertNull(scanner.next(),
444          "Major compaction should have purged the deleted row and its delete marker");
445      }
446
447      stopRegionServerAndWait(offlineRS.getServerName());
448
449      String secondFullBackupId = fullTableBackup(tables);
450      assertTrue(checkSucceeded(secondFullBackupId), "Second full backup should succeed");
451      String incrBackupId = incrementalTableBackup(tables);
452      assertTrue(checkSucceeded(incrBackupId), "Incremental backup should succeed");
453
454      TableName restoredTable = TableName.valueOf("table1_deleted_row_not_resurrected");
455      restore(incrBackupId, table1, restoredTable);
456      try (Table restored = TEST_UTIL.getConnection().getTable(restoredTable)) {
457        assertTrue(restored.get(new Get(deletedRow)).isEmpty(),
458          "Deleted row was brought back by the WALs of the offline RS");
459      }
460      assertEquals(TEST_UTIL.countRows(table1), TEST_UTIL.countRows(restoredTable),
461        "Restored table should match the source table");
462    }
463  }
464}