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 java.io.IOException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Objects;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileStatus;
030import org.apache.hadoop.fs.FileSystem;
031import org.apache.hadoop.fs.LocatedFileStatus;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.fs.RemoteIterator;
034import org.apache.hadoop.hbase.HBaseConfiguration;
035import org.apache.hadoop.hbase.HBaseTestingUtil;
036import org.apache.hadoop.hbase.HConstants;
037import org.apache.hadoop.hbase.NamespaceDescriptor;
038import org.apache.hadoop.hbase.TableName;
039import org.apache.hadoop.hbase.backup.BackupInfo.BackupPhase;
040import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
041import org.apache.hadoop.hbase.backup.impl.BackupAdminImpl;
042import org.apache.hadoop.hbase.backup.impl.BackupManager;
043import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
044import org.apache.hadoop.hbase.backup.impl.FullTableBackupClient;
045import org.apache.hadoop.hbase.backup.impl.IncrementalBackupManager;
046import org.apache.hadoop.hbase.backup.impl.IncrementalTableBackupClient;
047import org.apache.hadoop.hbase.backup.util.BackupUtils;
048import org.apache.hadoop.hbase.client.Admin;
049import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
050import org.apache.hadoop.hbase.client.Connection;
051import org.apache.hadoop.hbase.client.ConnectionFactory;
052import org.apache.hadoop.hbase.client.Durability;
053import org.apache.hadoop.hbase.client.Put;
054import org.apache.hadoop.hbase.client.Table;
055import org.apache.hadoop.hbase.client.TableDescriptor;
056import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
057import org.apache.hadoop.hbase.master.cleaner.LogCleaner;
058import org.apache.hadoop.hbase.master.cleaner.TimeToLiveLogCleaner;
059import org.apache.hadoop.hbase.regionserver.LogRoller;
060import org.apache.hadoop.hbase.security.HadoopSecurityEnabledUserProviderForTesting;
061import org.apache.hadoop.hbase.security.UserProvider;
062import org.apache.hadoop.hbase.security.access.SecureTestUtil;
063import org.apache.hadoop.hbase.snapshot.SnapshotTestingUtils;
064import org.apache.hadoop.hbase.util.Bytes;
065import org.apache.hadoop.hbase.util.CommonFSUtils;
066import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
067import org.apache.hadoop.hbase.wal.AbstractFSWALProvider;
068import org.apache.hadoop.hbase.wal.WALFactory;
069import org.junit.AfterClass;
070import org.junit.Before;
071import org.junit.BeforeClass;
072import org.junit.jupiter.api.AfterAll;
073import org.junit.jupiter.api.BeforeAll;
074import org.junit.jupiter.api.BeforeEach;
075import org.slf4j.Logger;
076import org.slf4j.LoggerFactory;
077
078/**
079 * This class is only a base for other integration-level backup tests. Do not add tests here.
080 * TestBackupSmallTests is where tests that don't require bring machines up/down should go All other
081 * tests should have their own classes and extend this one
082 */
083public class TestBackupBase {
084  private static final Logger LOG = LoggerFactory.getLogger(TestBackupBase.class);
085
086  protected static HBaseTestingUtil TEST_UTIL;
087  protected static HBaseTestingUtil TEST_UTIL2;
088  protected static Configuration conf1;
089  protected static Configuration conf2;
090
091  protected static TableName table1 = TableName.valueOf("table1");
092  protected static TableDescriptor table1Desc;
093  protected static TableName table2 = TableName.valueOf("table2");
094  protected static TableName table3 = TableName.valueOf("table3");
095  protected static TableName table4 = TableName.valueOf("table4");
096
097  protected static TableName table1_restore = TableName.valueOf("default:table1");
098  protected static TableName table2_restore = TableName.valueOf("ns2:table2");
099  protected static TableName table3_restore = TableName.valueOf("ns3:table3_restore");
100
101  protected static final int NB_ROWS_IN_BATCH = 99;
102  protected static final byte[] qualName = Bytes.toBytes("q1");
103  protected static final byte[] famName = Bytes.toBytes("f");
104
105  protected static String BACKUP_ROOT_DIR;
106  protected static String BACKUP_REMOTE_ROOT_DIR;
107  protected static String provider = "defaultProvider";
108  protected static boolean secure = false;
109
110  protected static boolean autoRestoreOnFailure;
111  protected static boolean useSecondCluster;
112
113  static class IncrementalTableBackupClientForTest extends IncrementalTableBackupClient {
114    public IncrementalTableBackupClientForTest() {
115    }
116
117    public IncrementalTableBackupClientForTest(Connection conn, String backupId,
118      BackupRequest request) throws IOException {
119      super(conn, backupId, request);
120    }
121
122    @BeforeEach
123    @Before
124    public void ensurePreviousBackupTestsAreCleanedUp() throws Exception {
125      // Every operation here may not be necessary for any given test,
126      // some often being no-ops. the goal is to help ensure atomicity
127      // of that tests that implement TestBackupBase
128      try (BackupAdmin backupAdmin = getBackupAdmin()) {
129        backupManager.finishBackupSession();
130        backupAdmin.listBackupSets().forEach(backupSet -> {
131          try {
132            backupAdmin.deleteBackupSet(backupSet.getName());
133          } catch (IOException ignored) {
134          }
135        });
136      } catch (Exception ignored) {
137      }
138      Arrays.stream(TEST_UTIL.getAdmin().listTableNames())
139        .filter(tableName -> !tableName.isSystemTable()).forEach(tableName -> {
140          try {
141            TEST_UTIL.truncateTable(tableName);
142          } catch (IOException ignored) {
143          }
144        });
145      TEST_UTIL.getMiniHBaseCluster().getRegionServerThreads().forEach(rst -> {
146        try {
147          LogRoller walRoller = rst.getRegionServer().getWalRoller();
148          walRoller.requestRollAll();
149          walRoller.waitUntilWalRollFinished();
150        } catch (Exception ignored) {
151        }
152      });
153    }
154
155    @Override
156    public void execute() throws IOException {
157      // case INCREMENTAL_COPY:
158      try {
159        // case PREPARE_INCREMENTAL:
160        failStageIf(Stage.stage_0);
161        beginBackup(backupManager, backupInfo);
162
163        failStageIf(Stage.stage_1);
164        backupInfo.setPhase(BackupPhase.PREPARE_INCREMENTAL);
165        LOG.debug("For incremental backup, current table set is "
166          + backupManager.getIncrementalBackupTableSet());
167        newTimestamps = ((IncrementalBackupManager) backupManager).getIncrBackupLogFileMap();
168        // copy out the table and region info files for each table
169        BackupUtils.copyTableRegionInfo(conn, backupInfo, conf);
170        // convert WAL to HFiles and copy them to .tmp under BACKUP_ROOT
171        convertWALsToHFiles();
172        incrementalCopyHFiles(new String[] { getBulkOutputDir().toString() },
173          backupInfo.getBackupRootDir());
174        failStageIf(Stage.stage_2);
175
176        // case INCR_BACKUP_COMPLETE:
177        // set overall backup status: complete. Here we make sure to complete the backup.
178        // After this checkpoint, even if entering cancel process, will let the backup finished
179        // Set the previousTimestampMap which is before this current log roll to the manifest.
180        Map<TableName, Map<String, Long>> previousTimestampMap =
181          backupManager.readLogTimestampMap();
182        backupInfo.setIncrTimestampMap(previousTimestampMap);
183
184        // The table list in backupInfo is good for both full backup and incremental backup.
185        // For incremental backup, it contains the incremental backup table set.
186        backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps);
187        failStageIf(Stage.stage_3);
188
189        Map<TableName, Map<String, Long>> newTableSetTimestampMap =
190          backupManager.readLogTimestampMap();
191
192        Long newStartCode =
193          BackupUtils.getMinValue(BackupUtils.getRSLogTimestampMins(newTableSetTimestampMap));
194        backupManager.writeBackupStartCode(newStartCode);
195
196        handleBulkLoad(backupInfo.getTableNames());
197        failStageIf(Stage.stage_4);
198
199        // backup complete
200        completeBackup(conn, backupInfo, BackupType.INCREMENTAL, conf);
201
202      } catch (Exception e) {
203        failBackup(conn, backupInfo, backupManager, e, "Unexpected Exception : ",
204          BackupType.INCREMENTAL, conf);
205        throw new IOException(e);
206      }
207    }
208  }
209
210  static class FullTableBackupClientForTest extends FullTableBackupClient {
211    public FullTableBackupClientForTest() {
212    }
213
214    public FullTableBackupClientForTest(Connection conn, String backupId, BackupRequest request)
215      throws IOException {
216      super(conn, backupId, request);
217    }
218
219    @Override
220    public void execute() throws IOException {
221      // Get the stage ID to fail on
222      try (Admin admin = conn.getAdmin()) {
223        // Begin BACKUP
224        beginBackup(backupManager, backupInfo);
225        failStageIf(Stage.stage_0);
226        String savedStartCode;
227        boolean firstBackup;
228        // do snapshot for full table backup
229        savedStartCode = backupManager.readBackupStartCode();
230        firstBackup = savedStartCode == null || Long.parseLong(savedStartCode) == 0L;
231        if (firstBackup) {
232          // This is our first backup. Let's put some marker to system table so that we can hold the
233          // logs while we do the backup.
234          backupManager.writeBackupStartCode(0L);
235        }
236        failStageIf(Stage.stage_1);
237        // We roll log here before we do the snapshot. It is possible there is duplicate data
238        // in the log that is already in the snapshot. But if we do it after the snapshot, we
239        // could have data loss.
240        // A better approach is to do the roll log on each RS in the same global procedure as
241        // the snapshot.
242        LOG.info("Execute roll log procedure for full backup ...");
243
244        BackupUtils.logRoll(conn, backupInfo.getBackupRootDir(), conf);
245        failStageIf(Stage.stage_2);
246        newTimestamps = backupManager.readRegionServerLastLogRollResult();
247
248        // SNAPSHOT_TABLES:
249        backupInfo.setPhase(BackupPhase.SNAPSHOT);
250        for (TableName tableName : tableList) {
251          String snapshotName = "snapshot_" + Long.toString(EnvironmentEdgeManager.currentTime())
252            + "_" + tableName.getNamespaceAsString() + "_" + tableName.getQualifierAsString();
253
254          snapshotTable(admin, tableName, snapshotName);
255          backupInfo.setSnapshotName(tableName, snapshotName);
256        }
257        failStageIf(Stage.stage_3);
258        // SNAPSHOT_COPY:
259        // do snapshot copy
260        LOG.debug("snapshot copy for " + backupId);
261        snapshotCopy(backupInfo);
262        // Updates incremental backup table set
263        backupManager.addIncrementalBackupTableSet(backupInfo.getTables());
264
265        // BACKUP_COMPLETE:
266        // set overall backup status: complete. Here we make sure to complete the backup.
267        // After this checkpoint, even if entering cancel process, will let the backup finished
268        backupInfo.setState(BackupState.COMPLETE);
269        // The table list in backupInfo is good for both full backup and incremental backup.
270        // For incremental backup, it contains the incremental backup table set.
271        backupManager.writeRegionServerLogTimestamp(backupInfo.getTables(), newTimestamps);
272
273        Map<TableName, Map<String, Long>> newTableSetTimestampMap =
274          backupManager.readLogTimestampMap();
275
276        Long newStartCode =
277          BackupUtils.getMinValue(BackupUtils.getRSLogTimestampMins(newTableSetTimestampMap));
278        backupManager.writeBackupStartCode(newStartCode);
279        failStageIf(Stage.stage_4);
280        // backup complete
281        completeBackup(conn, backupInfo, BackupType.FULL, conf);
282
283      } catch (Exception e) {
284
285        if (autoRestoreOnFailure) {
286          failBackup(conn, backupInfo, backupManager, e, "Unexpected BackupException : ",
287            BackupType.FULL, conf);
288        }
289        throw new IOException(e);
290      }
291    }
292  }
293
294  public static void setUpHelper() throws Exception {
295    BACKUP_ROOT_DIR = Path.SEPARATOR + "backupUT";
296    BACKUP_REMOTE_ROOT_DIR = Path.SEPARATOR + "backupUT";
297
298    if (secure) {
299      // set the always on security provider
300      UserProvider.setUserProviderForTesting(TEST_UTIL.getConfiguration(),
301        HadoopSecurityEnabledUserProviderForTesting.class);
302      // setup configuration
303      SecureTestUtil.enableSecurity(TEST_UTIL.getConfiguration());
304    }
305    conf1.setBoolean(BackupRestoreConstants.BACKUP_ENABLE_KEY, true);
306    BackupManager.decorateMasterConfiguration(conf1);
307    BackupManager.decorateRegionServerConfiguration(conf1);
308    conf1.set(HConstants.ZOOKEEPER_ZNODE_PARENT, "/1");
309    // Set TTL for old WALs to 1 sec to enforce fast cleaning of an archived
310    // WAL files
311    conf1.setLong(TimeToLiveLogCleaner.TTL_CONF_KEY, 1000);
312    conf1.setLong(LogCleaner.OLD_WALS_CLEANER_THREAD_TIMEOUT_MSEC, 1000);
313
314    // Set MultiWAL (with 2 default WAL files per RS)
315    conf1.set(WALFactory.WAL_PROVIDER, provider);
316    TEST_UTIL.startMiniCluster();
317    conf1 = TEST_UTIL.getConfiguration();
318    TEST_UTIL.startMiniMapReduceCluster();
319
320    if (useSecondCluster) {
321      conf2 = HBaseConfiguration.create(conf1);
322      conf2.set(HConstants.ZOOKEEPER_ZNODE_PARENT, "/2");
323      TEST_UTIL2 = new HBaseTestingUtil(conf2);
324      TEST_UTIL2.setZkCluster(TEST_UTIL.getZkCluster());
325      TEST_UTIL2.startMiniDFSCluster(3);
326      String root2 = TEST_UTIL2.getConfiguration().get("fs.defaultFS");
327      Path p = new Path(new Path(root2), "/tmp/wal");
328      CommonFSUtils.setWALRootDir(TEST_UTIL2.getConfiguration(), p);
329      TEST_UTIL2.startMiniCluster();
330    }
331
332    BACKUP_ROOT_DIR =
333      new Path(new Path(TEST_UTIL.getConfiguration().get("fs.defaultFS")), BACKUP_ROOT_DIR)
334        .toString();
335    LOG.info("ROOTDIR " + BACKUP_ROOT_DIR);
336    if (useSecondCluster) {
337      BACKUP_REMOTE_ROOT_DIR = new Path(
338        new Path(TEST_UTIL2.getConfiguration().get("fs.defaultFS")) + BACKUP_REMOTE_ROOT_DIR)
339        .toString();
340      LOG.info("REMOTE ROOTDIR " + BACKUP_REMOTE_ROOT_DIR);
341    }
342    createTables();
343    populateFromMasterConfig(TEST_UTIL.getHBaseCluster().getMaster().getConfiguration(), conf1);
344  }
345
346  /**
347   * Setup Cluster with appropriate configurations before running tests.
348   * @throws Exception if starting the mini cluster or setting up the tables fails
349   */
350  @BeforeAll
351  @BeforeClass
352  public static void setUp() throws Exception {
353    TEST_UTIL = new HBaseTestingUtil();
354    conf1 = TEST_UTIL.getConfiguration();
355    autoRestoreOnFailure = true;
356    useSecondCluster = false;
357    setUpHelper();
358  }
359
360  private static void populateFromMasterConfig(Configuration masterConf, Configuration conf) {
361    Iterator<Entry<String, String>> it = masterConf.iterator();
362    while (it.hasNext()) {
363      Entry<String, String> e = it.next();
364      conf.set(e.getKey(), e.getValue());
365    }
366  }
367
368  @AfterAll
369  @AfterClass
370  public static void tearDown() throws Exception {
371    try {
372      SnapshotTestingUtils.deleteAllSnapshots(TEST_UTIL.getAdmin());
373    } catch (Exception e) {
374    }
375    SnapshotTestingUtils.deleteArchiveDirectory(TEST_UTIL);
376    if (useSecondCluster) {
377      TEST_UTIL2.shutdownMiniCluster();
378    }
379    TEST_UTIL.shutdownMiniCluster();
380    TEST_UTIL.shutdownMiniMapReduceCluster();
381    autoRestoreOnFailure = true;
382    useSecondCluster = false;
383  }
384
385  Table insertIntoTable(Connection conn, TableName table, byte[] family, int id, int numRows)
386    throws IOException {
387    Table t = conn.getTable(table);
388    Put p1;
389    for (int i = 0; i < numRows; i++) {
390      p1 = new Put(Bytes.toBytes("row-" + table + "-" + id + "-" + i));
391      p1.addColumn(family, qualName, Bytes.toBytes("val" + i));
392      t.put(p1);
393    }
394    return t;
395  }
396
397  protected BackupRequest createBackupRequest(BackupType type, List<TableName> tables,
398    String path) {
399    return createBackupRequest(type, tables, path, false);
400  }
401
402  protected BackupRequest createBackupRequest(BackupType type, List<TableName> tables, String path,
403    boolean noChecksumVerify) {
404    BackupRequest.Builder builder = new BackupRequest.Builder();
405    BackupRequest request = builder.withBackupType(type).withTableList(tables)
406      .withTargetRootDir(path).withNoChecksumVerify(noChecksumVerify).build();
407    return request;
408  }
409
410  protected String backupTables(BackupType type, List<TableName> tables, String path)
411    throws IOException {
412    Connection conn = null;
413    BackupAdmin badmin = null;
414    String backupId;
415    try {
416      conn = ConnectionFactory.createConnection(conf1);
417      badmin = new BackupAdminImpl(conn);
418      BackupRequest request = createBackupRequest(type, new ArrayList<>(tables), path);
419      backupId = badmin.backupTables(request);
420    } finally {
421      if (badmin != null) {
422        badmin.close();
423      }
424      if (conn != null) {
425        conn.close();
426      }
427    }
428    return backupId;
429  }
430
431  protected String fullTableBackup(List<TableName> tables) throws IOException {
432    return backupTables(BackupType.FULL, tables, BACKUP_ROOT_DIR);
433  }
434
435  protected String incrementalTableBackup(List<TableName> tables) throws IOException {
436    return backupTables(BackupType.INCREMENTAL, tables, BACKUP_ROOT_DIR);
437  }
438
439  protected static void loadTable(Table table) throws Exception {
440    Put p; // 100 + 1 row to t1_syncup
441    for (int i = 0; i < NB_ROWS_IN_BATCH; i++) {
442      p = new Put(Bytes.toBytes("row" + i));
443      p.setDurability(Durability.SKIP_WAL);
444      p.addColumn(famName, qualName, Bytes.toBytes("val" + i));
445      table.put(p);
446    }
447  }
448
449  protected static void createTables() throws Exception {
450    long tid = EnvironmentEdgeManager.currentTime();
451    table1 = TableName.valueOf("test-" + tid);
452    Admin ha = TEST_UTIL.getAdmin();
453
454    // Create namespaces
455    ha.createNamespace(NamespaceDescriptor.create("ns1").build());
456    ha.createNamespace(NamespaceDescriptor.create("ns2").build());
457    ha.createNamespace(NamespaceDescriptor.create("ns3").build());
458    ha.createNamespace(NamespaceDescriptor.create("ns4").build());
459
460    TableDescriptor desc = TableDescriptorBuilder.newBuilder(table1)
461      .setColumnFamily(ColumnFamilyDescriptorBuilder.of(famName)).build();
462    ha.createTable(desc);
463    table1Desc = desc;
464    Connection conn = ConnectionFactory.createConnection(conf1);
465    Table table = conn.getTable(table1);
466    loadTable(table);
467    table.close();
468    table2 = TableName.valueOf("ns2:test-" + tid + 1);
469    desc = TableDescriptorBuilder.newBuilder(table2)
470      .setColumnFamily(ColumnFamilyDescriptorBuilder.of(famName)).build();
471    ha.createTable(desc);
472    table = conn.getTable(table2);
473    loadTable(table);
474    table.close();
475    table3 = TableName.valueOf("ns3:test-" + tid + 2);
476    table = TEST_UTIL.createTable(table3, famName);
477    table.close();
478    table4 = TableName.valueOf("ns4:test-" + tid + 3);
479    table = TEST_UTIL.createTable(table4, famName);
480    table.close();
481    ha.close();
482    conn.close();
483  }
484
485  protected boolean checkSucceeded(String backupId) throws IOException {
486    BackupInfo status = getBackupInfo(backupId);
487
488    if (status == null) {
489      return false;
490    }
491
492    return status.getState() == BackupState.COMPLETE;
493  }
494
495  protected boolean checkFailed(String backupId) throws IOException {
496    BackupInfo status = getBackupInfo(backupId);
497
498    if (status == null) {
499      return false;
500    }
501
502    return status.getState() == BackupState.FAILED;
503  }
504
505  private BackupInfo getBackupInfo(String backupId) throws IOException {
506    try (BackupSystemTable table = new BackupSystemTable(TEST_UTIL.getConnection())) {
507      BackupInfo status = table.readBackupInfo(backupId);
508      return status;
509    }
510  }
511
512  protected static BackupAdmin getBackupAdmin() throws IOException {
513    return new BackupAdminImpl(TEST_UTIL.getConnection());
514  }
515
516  /**
517   * Helper method
518   */
519  protected List<TableName> toList(String... args) {
520    List<TableName> ret = new ArrayList<>();
521    for (int i = 0; i < args.length; i++) {
522      ret.add(TableName.valueOf(args[i]));
523    }
524    return ret;
525  }
526
527  protected List<FileStatus> getListOfWALFiles(Configuration c) throws IOException {
528    Path logRoot = new Path(CommonFSUtils.getWALRootDir(c), HConstants.HREGION_LOGDIR_NAME);
529    FileSystem fs = logRoot.getFileSystem(c);
530    RemoteIterator<LocatedFileStatus> it = fs.listFiles(logRoot, true);
531    List<FileStatus> logFiles = new ArrayList<FileStatus>();
532    while (it.hasNext()) {
533      LocatedFileStatus lfs = it.next();
534      if (lfs.isFile() && !AbstractFSWALProvider.isMetaFile(lfs.getPath())) {
535        logFiles.add(lfs);
536        LOG.info(Objects.toString(lfs));
537      }
538    }
539    return logFiles;
540  }
541
542  protected void dumpBackupDir() throws IOException {
543    // Dump Backup Dir
544    FileSystem fs = FileSystem.get(conf1);
545    RemoteIterator<LocatedFileStatus> it = fs.listFiles(new Path(BACKUP_ROOT_DIR), true);
546    while (it.hasNext()) {
547      LOG.debug(Objects.toString(it.next().getPath()));
548    }
549  }
550}