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.client;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.io.File;
025import java.io.IOException;
026import java.nio.file.Paths;
027import java.util.ArrayList;
028import java.util.Iterator;
029import java.util.List;
030import java.util.UUID;
031import java.util.regex.Pattern;
032import java.util.stream.Stream;
033import org.apache.commons.io.FileUtils;
034import org.apache.hadoop.conf.Configuration;
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.master.snapshot.SnapshotManager;
041import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy;
042import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
043import org.apache.hadoop.hbase.snapshot.SnapshotDoesNotExistException;
044import org.apache.hadoop.hbase.snapshot.SnapshotManifestV1;
045import org.apache.hadoop.hbase.snapshot.SnapshotManifestV2;
046import org.apache.hadoop.hbase.snapshot.SnapshotTestingUtils;
047import org.apache.hadoop.hbase.util.Bytes;
048import org.apache.hadoop.hbase.util.CommonFSUtils;
049import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
050import org.junit.jupiter.api.AfterAll;
051import org.junit.jupiter.api.AfterEach;
052import org.junit.jupiter.api.BeforeEach;
053import org.junit.jupiter.api.TestTemplate;
054import org.junit.jupiter.params.provider.Arguments;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
059
060import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
061
062/**
063 * This class tests that the use of a temporary snapshot directory supports snapshot functionality
064 * while the temporary directory is on a different file system than the root directory
065 * <p>
066 * This is an end-to-end test for the snapshot utility
067 */
068public abstract class SnapshotTemporaryDirectoryTestBase {
069
070  public static Stream<Arguments> parameters() {
071    return Stream.of(Arguments.of(SnapshotManifestV1.DESCRIPTOR_VERSION),
072      Arguments.of(SnapshotManifestV2.DESCRIPTOR_VERSION));
073  }
074
075  private final int manifestVersion;
076
077  public SnapshotTemporaryDirectoryTestBase(int manifestVersion) {
078    this.manifestVersion = manifestVersion;
079  }
080
081  private static final Logger LOG =
082    LoggerFactory.getLogger(SnapshotTemporaryDirectoryTestBase.class);
083  protected static final int NUM_RS = 2;
084  protected static String TEMP_DIR =
085    Paths.get("").toAbsolutePath().toString() + Path.SEPARATOR + UUID.randomUUID().toString();
086
087  protected static Admin admin;
088  protected static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
089  protected static final String STRING_TABLE_NAME = "test";
090  protected static final byte[] TEST_FAM = Bytes.toBytes("fam");
091  protected static final TableName TABLE_NAME = TableName.valueOf(STRING_TABLE_NAME);
092
093  protected static void setupConf(Configuration conf) throws IOException {
094    // disable the ui
095    conf.setInt("hbase.regionsever.info.port", -1);
096    // change the flush size to a small amount, regulating number of store files
097    conf.setInt("hbase.hregion.memstore.flush.size", 25000);
098    // so make sure we get a compaction when doing a load, but keep around some
099    // files in the store
100    conf.setInt("hbase.hstore.compaction.min", 10);
101    conf.setInt("hbase.hstore.compactionThreshold", 10);
102    // block writes if we get to 12 store files
103    conf.setInt("hbase.hstore.blockingStoreFiles", 12);
104    // Enable snapshot
105    conf.setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true);
106    conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY,
107      ConstantSizeRegionSplitPolicy.class.getName());
108    conf.set(SnapshotDescriptionUtils.SNAPSHOT_WORKING_DIR,
109      "file://" + new Path(TEMP_DIR, ".tmpDir").toUri());
110  }
111
112  @BeforeEach
113  public void setup() throws Exception {
114    TableDescriptor htd =
115      TableDescriptorBuilder.newBuilder(TABLE_NAME).setRegionReplication(getNumReplicas()).build();
116    UTIL.createTable(htd, new byte[][] { TEST_FAM }, UTIL.getConfiguration());
117  }
118
119  protected int getNumReplicas() {
120    return 1;
121  }
122
123  @AfterEach
124  public void tearDown() throws Exception {
125    UTIL.deleteTable(TABLE_NAME);
126    SnapshotTestingUtils.deleteAllSnapshots(UTIL.getAdmin());
127    SnapshotTestingUtils.deleteArchiveDirectory(UTIL);
128  }
129
130  @AfterAll
131  public static void cleanupTest() {
132    try {
133      UTIL.shutdownMiniCluster();
134      FileUtils.deleteDirectory(new File(TEMP_DIR));
135    } catch (Exception e) {
136      LOG.warn("failure shutting down cluster", e);
137    }
138  }
139
140  @TestTemplate
141  public void testRestoreDisabledSnapshot() throws IOException, InterruptedException {
142    long tid = EnvironmentEdgeManager.currentTime();
143    TableName tableName = TableName.valueOf("testtb-" + tid);
144    String emptySnapshot = "emptySnaptb-" + tid;
145    String snapshotName0 = "snaptb0-" + tid;
146    String snapshotName1 = "snaptb1-" + tid;
147    int snapshot0Rows;
148    int snapshot1Rows;
149
150    // create Table and disable it
151    SnapshotTestingUtils.createTable(UTIL, tableName, getNumReplicas(), TEST_FAM);
152    admin.disableTable(tableName);
153
154    // take an empty snapshot
155    takeSnapshot(tableName, emptySnapshot, true);
156
157    // enable table and insert data
158    admin.enableTable(tableName);
159    SnapshotTestingUtils.loadData(UTIL, tableName, 500, TEST_FAM);
160    try (Table table = UTIL.getConnection().getTable(tableName)) {
161      snapshot0Rows = UTIL.countRows(table);
162    }
163    admin.disableTable(tableName);
164
165    // take a snapshot
166    takeSnapshot(tableName, snapshotName0, true);
167
168    // enable table and insert more data
169    admin.enableTable(tableName);
170    SnapshotTestingUtils.loadData(UTIL, tableName, 500, TEST_FAM);
171    try (Table table = UTIL.getConnection().getTable(tableName)) {
172      snapshot1Rows = UTIL.countRows(table);
173    }
174
175    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot1Rows);
176    admin.disableTable(tableName);
177    takeSnapshot(tableName, snapshotName1, true);
178
179    // Restore from snapshot-0
180    admin.restoreSnapshot(snapshotName0);
181    admin.enableTable(tableName);
182    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot0Rows);
183    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
184
185    // Restore from emptySnapshot
186    admin.disableTable(tableName);
187    admin.restoreSnapshot(emptySnapshot);
188    admin.enableTable(tableName);
189    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, 0);
190    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
191
192    // Restore from snapshot-1
193    admin.disableTable(tableName);
194    admin.restoreSnapshot(snapshotName1);
195    admin.enableTable(tableName);
196    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot1Rows);
197    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
198
199    // Restore from snapshot-1
200    UTIL.deleteTable(tableName);
201    admin.restoreSnapshot(snapshotName1);
202    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot1Rows);
203    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
204  }
205
206  @TestTemplate
207  public void testRestoreEnabledSnapshot() throws IOException, InterruptedException {
208    long tid = EnvironmentEdgeManager.currentTime();
209    TableName tableName = TableName.valueOf("testtb-" + tid);
210    String emptySnapshot = "emptySnaptb-" + tid;
211    String snapshotName0 = "snaptb0-" + tid;
212    String snapshotName1 = "snaptb1-" + tid;
213    int snapshot0Rows;
214    int snapshot1Rows;
215
216    // create Table
217    SnapshotTestingUtils.createTable(UTIL, tableName, getNumReplicas(), TEST_FAM);
218
219    // take an empty snapshot
220    takeSnapshot(tableName, emptySnapshot, false);
221
222    // Insert data
223    SnapshotTestingUtils.loadData(UTIL, tableName, 500, TEST_FAM);
224    try (Table table = UTIL.getConnection().getTable(tableName)) {
225      snapshot0Rows = UTIL.countRows(table);
226    }
227
228    // take a snapshot
229    takeSnapshot(tableName, snapshotName0, false);
230
231    // Insert more data
232    SnapshotTestingUtils.loadData(UTIL, tableName, 500, TEST_FAM);
233    try (Table table = UTIL.getConnection().getTable(tableName)) {
234      snapshot1Rows = UTIL.countRows(table);
235    }
236
237    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot1Rows);
238    takeSnapshot(tableName, snapshotName1, false);
239
240    // Restore from snapshot-0
241    admin.disableTable(tableName);
242    admin.restoreSnapshot(snapshotName0);
243    admin.enableTable(tableName);
244    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot0Rows);
245    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
246
247    // Restore from emptySnapshot
248    admin.disableTable(tableName);
249    admin.restoreSnapshot(emptySnapshot);
250    admin.enableTable(tableName);
251    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, 0);
252    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
253
254    // Restore from snapshot-1
255    admin.disableTable(tableName);
256    admin.restoreSnapshot(snapshotName1);
257    admin.enableTable(tableName);
258    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot1Rows);
259    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
260
261    // Restore from snapshot-1
262    UTIL.deleteTable(tableName);
263    admin.restoreSnapshot(snapshotName1);
264    SnapshotTestingUtils.verifyRowCount(UTIL, tableName, snapshot1Rows);
265    SnapshotTestingUtils.verifyReplicasCameOnline(tableName, admin, getNumReplicas());
266  }
267
268  /**
269   * Test snapshotting a table that is offline
270   * @throws Exception if snapshot does not complete successfully
271   */
272  @TestTemplate
273  public void testOfflineTableSnapshot() throws Exception {
274    Admin admin = UTIL.getAdmin();
275    // make sure we don't fail on listing snapshots
276    SnapshotTestingUtils.assertNoSnapshots(admin);
277
278    // put some stuff in the table
279    Table table = UTIL.getConnection().getTable(TABLE_NAME);
280    UTIL.loadTable(table, TEST_FAM, false);
281
282    LOG.debug("FS state before disable:");
283    CommonFSUtils.logFileSystemState(UTIL.getTestFileSystem(),
284      CommonFSUtils.getRootDir(UTIL.getConfiguration()), LOG);
285    // XXX if this is flakey, might want to consider using the async version and looping as
286    // disableTable can succeed and still timeout.
287    admin.disableTable(TABLE_NAME);
288
289    LOG.debug("FS state before snapshot:");
290    CommonFSUtils.logFileSystemState(UTIL.getTestFileSystem(),
291      CommonFSUtils.getRootDir(UTIL.getConfiguration()), LOG);
292
293    // take a snapshot of the disabled table
294    final String SNAPSHOT_NAME = "offlineTableSnapshot";
295    String snapshot = SNAPSHOT_NAME;
296    takeSnapshot(TABLE_NAME, SNAPSHOT_NAME, true);
297    LOG.debug("Snapshot completed.");
298
299    // make sure we have the snapshot
300    List<SnapshotDescription> snapshots =
301      SnapshotTestingUtils.assertOneSnapshotThatMatches(admin, snapshot, TABLE_NAME);
302
303    // make sure its a valid snapshot
304    FileSystem fs = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getFileSystem();
305    Path rootDir = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
306    LOG.debug("FS state after snapshot:");
307    CommonFSUtils.logFileSystemState(UTIL.getTestFileSystem(),
308      CommonFSUtils.getRootDir(UTIL.getConfiguration()), LOG);
309
310    SnapshotTestingUtils.confirmSnapshotValid(
311      ProtobufUtil.createHBaseProtosSnapshotDesc(snapshots.get(0)), TABLE_NAME, TEST_FAM, rootDir,
312      admin, fs);
313
314    admin.deleteSnapshot(snapshot);
315    SnapshotTestingUtils.assertNoSnapshots(admin);
316  }
317
318  /**
319   * Tests that snapshot has correct contents by taking snapshot, cloning it, then affirming the
320   * contents of the original and cloned table match
321   * @throws Exception if snapshot does not complete successfully
322   */
323  @TestTemplate
324  public void testSnapshotCloneContents() throws Exception {
325    // make sure we don't fail on listing snapshots
326    SnapshotTestingUtils.assertNoSnapshots(admin);
327
328    // put some stuff in the table
329    Table table = UTIL.getConnection().getTable(TABLE_NAME);
330    UTIL.loadTable(table, TEST_FAM);
331    table.close();
332
333    String snapshot1 = "TableSnapshot1";
334    takeSnapshot(TABLE_NAME, snapshot1, false);
335    LOG.debug("Snapshot1 completed.");
336
337    TableName clone = TableName.valueOf("Table1Clone");
338    admin.cloneSnapshot(snapshot1, clone, false);
339
340    Scan original = new Scan();
341    Scan cloned = new Scan();
342    ResultScanner originalScan = admin.getConnection().getTable(TABLE_NAME).getScanner(original);
343    ResultScanner clonedScan =
344      admin.getConnection().getTable(TableName.valueOf("Table1Clone")).getScanner(cloned);
345
346    Iterator<Result> i = originalScan.iterator();
347    Iterator<Result> i2 = clonedScan.iterator();
348    assertTrue(i.hasNext());
349    while (i.hasNext()) {
350      assertTrue(i2.hasNext());
351      assertEquals(Bytes.toString(i.next().getValue(TEST_FAM, new byte[] {})),
352        Bytes.toString(i2.next().getValue(TEST_FAM, new byte[] {})));
353    }
354    assertFalse(i2.hasNext());
355    admin.deleteSnapshot(snapshot1);
356    UTIL.deleteTable(clone);
357    admin.close();
358  }
359
360  @TestTemplate
361  public void testOfflineTableSnapshotWithEmptyRegion() throws Exception {
362    // test with an empty table with one region
363
364    // make sure we don't fail on listing snapshots
365    SnapshotTestingUtils.assertNoSnapshots(admin);
366
367    LOG.debug("FS state before disable:");
368    CommonFSUtils.logFileSystemState(UTIL.getTestFileSystem(),
369      CommonFSUtils.getRootDir(UTIL.getConfiguration()), LOG);
370    admin.disableTable(TABLE_NAME);
371
372    LOG.debug("FS state before snapshot:");
373    CommonFSUtils.logFileSystemState(UTIL.getTestFileSystem(),
374      CommonFSUtils.getRootDir(UTIL.getConfiguration()), LOG);
375
376    // take a snapshot of the disabled table
377    String snapshot = "testOfflineTableSnapshotWithEmptyRegion";
378    takeSnapshot(TABLE_NAME, snapshot, true);
379    LOG.debug("Snapshot completed.");
380
381    // make sure we have the snapshot
382    List<SnapshotDescription> snapshots =
383      SnapshotTestingUtils.assertOneSnapshotThatMatches(admin, snapshot, TABLE_NAME);
384
385    // make sure its a valid snapshot
386    FileSystem fs = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getFileSystem();
387    Path rootDir = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
388    LOG.debug("FS state after snapshot:");
389    CommonFSUtils.logFileSystemState(UTIL.getTestFileSystem(),
390      CommonFSUtils.getRootDir(UTIL.getConfiguration()), LOG);
391
392    List<byte[]> emptyCfs = Lists.newArrayList(TEST_FAM); // no file in the region
393    List<byte[]> nonEmptyCfs = Lists.newArrayList();
394    SnapshotTestingUtils.confirmSnapshotValid(
395      ProtobufUtil.createHBaseProtosSnapshotDesc(snapshots.get(0)), TABLE_NAME, nonEmptyCfs,
396      emptyCfs, rootDir, admin, fs);
397
398    admin.deleteSnapshot(snapshot);
399    SnapshotTestingUtils.assertNoSnapshots(admin);
400  }
401
402  // Ensures that the snapshot is transferred to the proper completed snapshot directory
403  @TestTemplate
404  public void testEnsureTemporaryDirectoryTransfer() throws Exception {
405    Admin admin = UTIL.getAdmin();
406    TableName tableName2 = TableName.valueOf("testListTableSnapshots");
407    try {
408      TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName2).build();
409      UTIL.createTable(htd, new byte[][] { TEST_FAM }, UTIL.getConfiguration());
410
411      String table1Snapshot1 = "Table1Snapshot1";
412      takeSnapshot(TABLE_NAME, table1Snapshot1, false);
413      LOG.debug("Snapshot1 completed.");
414
415      String table1Snapshot2 = "Table1Snapshot2";
416      takeSnapshot(TABLE_NAME, table1Snapshot2, false);
417      LOG.debug("Snapshot2 completed.");
418
419      String table2Snapshot1 = "Table2Snapshot1";
420      takeSnapshot(TABLE_NAME, table2Snapshot1, false);
421      LOG.debug("Table2Snapshot1 completed.");
422
423      List<SnapshotDescription> listTableSnapshots =
424        admin.listTableSnapshots(Pattern.compile("test.*"), Pattern.compile(".*"));
425      List<String> listTableSnapshotNames = new ArrayList<String>();
426      assertEquals(3, listTableSnapshots.size());
427      for (SnapshotDescription s : listTableSnapshots) {
428        listTableSnapshotNames.add(s.getName());
429      }
430      assertTrue(listTableSnapshotNames.contains(table1Snapshot1));
431      assertTrue(listTableSnapshotNames.contains(table1Snapshot2));
432      assertTrue(listTableSnapshotNames.contains(table2Snapshot1));
433    } finally {
434      try {
435        admin.deleteSnapshots(Pattern.compile("Table.*"));
436      } catch (SnapshotDoesNotExistException ignore) {
437      }
438      if (admin.tableExists(tableName2)) {
439        UTIL.deleteTable(tableName2);
440      }
441      admin.close();
442    }
443  }
444
445  private void takeSnapshot(TableName tableName, String snapshotName, boolean disabled)
446    throws IOException {
447    SnapshotType type = disabled ? SnapshotType.DISABLED : SnapshotType.FLUSH;
448    SnapshotDescription desc =
449      new SnapshotDescription(snapshotName, tableName, type, null, -1, manifestVersion, null);
450    admin.snapshot(desc);
451  }
452}