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.snapshot;
019
020import static org.apache.hadoop.util.ToolRunner.run;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertFalse;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.IOException;
026import java.lang.reflect.Method;
027import java.util.ArrayList;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Objects;
031import java.util.Set;
032import java.util.stream.Stream;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.fs.FileStatus;
035import org.apache.hadoop.fs.FileSystem;
036import org.apache.hadoop.fs.Path;
037import org.apache.hadoop.hbase.HBaseTestingUtil;
038import org.apache.hadoop.hbase.HConstants;
039import org.apache.hadoop.hbase.TableName;
040import org.apache.hadoop.hbase.client.Admin;
041import org.apache.hadoop.hbase.client.RegionInfo;
042import org.apache.hadoop.hbase.mob.MobUtils;
043import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
044import org.apache.hadoop.hbase.util.Bytes;
045import org.apache.hadoop.hbase.util.CommonFSUtils;
046import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
047import org.apache.hadoop.hbase.util.Pair;
048import org.junit.jupiter.api.AfterAll;
049import org.junit.jupiter.api.AfterEach;
050import org.junit.jupiter.api.BeforeEach;
051import org.junit.jupiter.api.TestInfo;
052import org.junit.jupiter.params.provider.Arguments;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
057import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
058
059public class ExportSnapshotTestBase {
060
061  private static final Logger LOG = LoggerFactory.getLogger(ExportSnapshotTestBase.class);
062
063  protected final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
064
065  protected final static byte[] FAMILY = Bytes.toBytes("cf");
066
067  protected String methodName;
068
069  protected TableName tableName;
070
071  protected String emptySnapshotName;
072
073  protected String snapshotName;
074
075  protected int tableNumFiles;
076
077  protected Admin admin;
078
079  protected boolean mob;
080
081  public static Stream<Arguments> parameters() {
082    return Stream.of(Arguments.of(false), Arguments.of(true));
083  }
084
085  protected ExportSnapshotTestBase(boolean mob) {
086    this.mob = mob;
087  }
088
089  @AfterAll
090  public static void tearDownAfterClass() throws Exception {
091    TEST_UTIL.shutdownMiniMapReduceCluster();
092    TEST_UTIL.shutdownMiniCluster();
093  }
094
095  /**
096   * Create a table and take a snapshot of the table used by the export test.
097   */
098  @BeforeEach
099  public void setUp(TestInfo testInfo) throws Exception {
100    this.admin = TEST_UTIL.getAdmin();
101
102    methodName = testInfo.getTestMethod().map(Method::getName).orElse(getClass().getSimpleName());
103
104    String suffix = mob ? methodName + "-mob" : methodName;
105    tableName = TableName.valueOf("testtb-" + suffix);
106    snapshotName = "snaptb0-" + suffix;
107    emptySnapshotName = "emptySnaptb0-" + suffix;
108
109    // create Table
110    createTable(this.tableName);
111
112    // Take an empty snapshot
113    admin.snapshot(emptySnapshotName, tableName);
114
115    // Add some rows
116    SnapshotTestingUtils.loadData(TEST_UTIL, tableName, 50, FAMILY);
117    tableNumFiles = admin.getRegions(tableName).size();
118
119    // take a snapshot
120    admin.snapshot(snapshotName, tableName);
121  }
122
123  protected final void createTable(TableName tableName) throws Exception {
124    if (mob) {
125      MobSnapshotTestingUtils.createPreSplitMobTable(TEST_UTIL, tableName, 2, FAMILY);
126    } else {
127      SnapshotTestingUtils.createPreSplitTable(TEST_UTIL, tableName, 2, FAMILY);
128    }
129  }
130
131  protected interface RegionPredicate {
132    boolean evaluate(final RegionInfo regionInfo);
133  }
134
135  protected final RegionPredicate getBypassRegionPredicate() {
136    if (mob) {
137      return MobUtils::isMobRegionInfo;
138    } else {
139      return null;
140    }
141  }
142
143  @AfterEach
144  public void tearDown() throws Exception {
145    for (TableName tn : TEST_UTIL.getAdmin().listTableNames()) {
146      TEST_UTIL.deleteTable(tn);
147    }
148    SnapshotTestingUtils.deleteAllSnapshots(TEST_UTIL.getAdmin());
149    SnapshotTestingUtils.deleteArchiveDirectory(TEST_UTIL);
150  }
151
152  protected final void testExportFileSystemState(final TableName tableName,
153    final String snapshotName, final String targetName, int filesExpected) throws Exception {
154    testExportFileSystemState(tableName, snapshotName, targetName, filesExpected,
155      getHdfsDestinationDir(), false);
156  }
157
158  protected final void testExportFileSystemState(final TableName tableName,
159    final String snapshotName, final String targetName, int filesExpected, Path copyDir,
160    boolean overwrite) throws Exception {
161    testExportFileSystemState(tableName, snapshotName, targetName, filesExpected, copyDir,
162      overwrite, false);
163  }
164
165  protected final void testExportFileSystemState(final TableName tableName,
166    final String snapshotName, final String targetName, int filesExpected, Path copyDir,
167    boolean overwrite, boolean resetTtl) throws Exception {
168    testExportFileSystemState(TEST_UTIL.getConfiguration(), tableName, snapshotName, targetName,
169      filesExpected, TEST_UTIL.getDefaultRootDirPath(), copyDir, overwrite, resetTtl,
170      getBypassRegionPredicate(), true, false);
171  }
172
173  /**
174   * Creates destination directory, runs ExportSnapshot() tool, and runs some verifications.
175   */
176  protected static void testExportFileSystemState(final Configuration conf,
177    final TableName tableName, final String snapshotName, final String targetName,
178    final int filesExpected, final Path srcDir, Path rawTgtDir, final boolean overwrite,
179    final boolean resetTtl, final RegionPredicate bypassregionPredicate, final boolean success,
180    final boolean checksumVerify) throws Exception {
181    FileSystem tgtFs = rawTgtDir.getFileSystem(conf);
182    FileSystem srcFs = srcDir.getFileSystem(conf);
183    Path tgtDir = rawTgtDir.makeQualified(tgtFs.getUri(), tgtFs.getWorkingDirectory());
184
185    // Export Snapshot
186    int res = runExportSnapshot(conf, snapshotName, targetName, srcDir, rawTgtDir, overwrite,
187      resetTtl, checksumVerify, true, true);
188    assertEquals(success ? 0 : 1, res, "success " + success + ", res=" + res);
189    if (!success) {
190      final Path targetDir = new Path(HConstants.SNAPSHOT_DIR_NAME, targetName);
191      assertFalse(tgtFs.exists(new Path(tgtDir, targetDir)),
192        tgtDir.toString() + " " + targetDir.toString());
193      return;
194    }
195    LOG.info("Exported snapshot");
196
197    // Verify File-System state
198    FileStatus[] rootFiles = tgtFs.listStatus(tgtDir);
199    assertEquals(filesExpected > 0 ? 2 : 1, rootFiles.length);
200    for (FileStatus fileStatus : rootFiles) {
201      String name = fileStatus.getPath().getName();
202      assertTrue(fileStatus.isDirectory(), fileStatus.toString());
203      assertTrue(name.equals(HConstants.SNAPSHOT_DIR_NAME)
204        || name.equals(HConstants.HFILE_ARCHIVE_DIRECTORY), name.toString());
205    }
206    LOG.info("Verified filesystem state");
207
208    // Compare the snapshot metadata and verify the hfiles
209    final Path snapshotDir = new Path(HConstants.SNAPSHOT_DIR_NAME, snapshotName);
210    final Path targetDir = new Path(HConstants.SNAPSHOT_DIR_NAME, targetName);
211    verifySnapshotDir(srcFs, new Path(srcDir, snapshotDir), tgtFs, new Path(tgtDir, targetDir));
212    Set<String> snapshotFiles =
213      verifySnapshot(conf, tgtFs, tgtDir, tableName, targetName, resetTtl, bypassregionPredicate);
214    assertEquals(filesExpected, snapshotFiles.size());
215  }
216
217  /*
218   * verify if the snapshot folder on file-system 1 match the one on file-system 2
219   */
220  protected static void verifySnapshotDir(final FileSystem fs1, final Path root1,
221    final FileSystem fs2, final Path root2) throws IOException {
222    assertEquals(listFiles(fs1, root1, root1), listFiles(fs2, root2, root2));
223  }
224
225  /*
226   * Verify if the files exists
227   */
228  protected static Set<String> verifySnapshot(final Configuration conf, final FileSystem fs,
229    final Path rootDir, final TableName tableName, final String snapshotName,
230    final boolean resetTtl, final RegionPredicate bypassregionPredicate) throws IOException {
231    final Path exportedSnapshot =
232      new Path(rootDir, new Path(HConstants.SNAPSHOT_DIR_NAME, snapshotName));
233    final Set<String> snapshotFiles = new HashSet<>();
234    final Path exportedArchive = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY);
235    SnapshotReferenceUtil.visitReferencedFiles(conf, fs, exportedSnapshot,
236      new SnapshotReferenceUtil.SnapshotVisitor() {
237        @Override
238        public void storeFile(final RegionInfo regionInfo, final String family,
239          final SnapshotRegionManifest.StoreFile storeFile) throws IOException {
240          if (bypassregionPredicate != null && bypassregionPredicate.evaluate(regionInfo)) {
241            return;
242          }
243
244          if (!storeFile.hasReference() && !StoreFileInfo.isReference(storeFile.getName())) {
245            String hfile = storeFile.getName();
246            snapshotFiles.add(hfile);
247            verifyNonEmptyFile(new Path(exportedArchive,
248              new Path(CommonFSUtils.getTableDir(new Path("./"), tableName),
249                new Path(regionInfo.getEncodedName(), new Path(family, hfile)))));
250          } else {
251            Pair<String, String> referredToRegionAndFile =
252              StoreFileInfo.getReferredToRegionAndFile(storeFile.getName());
253            String region = referredToRegionAndFile.getFirst();
254            String hfile = referredToRegionAndFile.getSecond();
255            snapshotFiles.add(hfile);
256            verifyNonEmptyFile(new Path(exportedArchive,
257              new Path(CommonFSUtils.getTableDir(new Path("./"), tableName),
258                new Path(region, new Path(family, hfile)))));
259          }
260        }
261
262        private void verifyNonEmptyFile(final Path path) throws IOException {
263          assertTrue(fs.exists(path), path + " should exists");
264          assertTrue(fs.getFileStatus(path).getLen() > 0, path + " should not be empty");
265        }
266      });
267
268    // Verify Snapshot description
269    SnapshotDescription desc = SnapshotDescriptionUtils.readSnapshotInfo(fs, exportedSnapshot);
270    assertTrue(desc.getName().equals(snapshotName));
271    assertTrue(desc.getTable().equals(tableName.getNameAsString()));
272    if (resetTtl) {
273      assertEquals(HConstants.DEFAULT_SNAPSHOT_TTL, desc.getTtl());
274    }
275    return snapshotFiles;
276  }
277
278  private static Set<String> listFiles(final FileSystem fs, final Path root, final Path dir)
279    throws IOException {
280    Set<String> files = new HashSet<>();
281    LOG.debug("List files in {} in root {} at {}", fs, root, dir);
282    int rootPrefix = root.makeQualified(fs.getUri(), fs.getWorkingDirectory()).toString().length();
283    FileStatus[] list = CommonFSUtils.listStatus(fs, dir);
284    if (list != null) {
285      for (FileStatus fstat : list) {
286        LOG.debug(Objects.toString(fstat.getPath()));
287        if (fstat.isDirectory()) {
288          files.addAll(listFiles(fs, root, fstat.getPath()));
289        } else {
290          files.add(fstat.getPath().makeQualified(fs.getUri(), fs.getWorkingDirectory()).toString()
291            .substring(rootPrefix));
292        }
293      }
294    }
295    return files;
296  }
297
298  protected final Path getHdfsDestinationDir() {
299    Path rootDir = TEST_UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
300    Path path =
301      new Path(new Path(rootDir, "export-test"), "export-" + EnvironmentEdgeManager.currentTime());
302    LOG.info("HDFS export destination path: " + path);
303    return path;
304  }
305
306  protected static Path getLocalDestinationDir(HBaseTestingUtil htu) {
307    Path path = htu.getDataTestDir("local-export-" + EnvironmentEdgeManager.currentTime());
308    try {
309      FileSystem fs = FileSystem.getLocal(htu.getConfiguration());
310      LOG.info("Local export destination path: " + path);
311      return path.makeQualified(fs.getUri(), fs.getWorkingDirectory());
312    } catch (IOException ioe) {
313      throw new RuntimeException(ioe);
314    }
315  }
316
317  protected static void removeExportDir(final Path path) throws IOException {
318    FileSystem fs = FileSystem.get(path.toUri(), new Configuration());
319    fs.delete(path, true);
320  }
321
322  protected static int runExportSnapshot(final Configuration conf, final String sourceSnapshotName,
323    final String targetSnapshotName, final Path srcDir, Path rawTgtDir, final boolean overwrite,
324    final boolean resetTtl, final boolean checksumVerify, final boolean noSourceVerify,
325    final boolean noTargetVerify) throws Exception {
326    FileSystem tgtFs = rawTgtDir.getFileSystem(conf);
327    FileSystem srcFs = srcDir.getFileSystem(conf);
328    Path tgtDir = rawTgtDir.makeQualified(tgtFs.getUri(), tgtFs.getWorkingDirectory());
329    LOG.info("tgtFsUri={}, tgtDir={}, rawTgtDir={}, srcFsUri={}, srcDir={}", tgtFs.getUri(), tgtDir,
330      rawTgtDir, srcFs.getUri(), srcDir);
331    List<String> opts = new ArrayList<>();
332    opts.add("--snapshot");
333    opts.add(sourceSnapshotName);
334    opts.add("--copy-to");
335    opts.add(tgtDir.toString());
336    if (!targetSnapshotName.equals(sourceSnapshotName)) {
337      opts.add("--target");
338      opts.add(targetSnapshotName);
339    }
340    if (overwrite) {
341      opts.add("--overwrite");
342    }
343    if (resetTtl) {
344      opts.add("--reset-ttl");
345    }
346    if (!checksumVerify) {
347      opts.add("--no-checksum-verify");
348    }
349    if (!noSourceVerify) {
350      opts.add("--no-source-verify");
351    }
352    if (!noTargetVerify) {
353      opts.add("--no-target-verify");
354    }
355
356    // Export Snapshot
357    return run(conf, new ExportSnapshot(), opts.toArray(new String[opts.size()]));
358  }
359}