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.assertTrue;
022
023import java.util.List;
024import java.util.regex.Pattern;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.fs.FileSystem;
027import org.apache.hadoop.fs.Path;
028import org.apache.hadoop.hbase.HBaseTestingUtil;
029import org.apache.hadoop.hbase.HConstants;
030import org.apache.hadoop.hbase.TableName;
031import org.apache.hadoop.hbase.master.snapshot.SnapshotManager;
032import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy;
033import org.apache.hadoop.hbase.snapshot.SnapshotTestingUtils;
034import org.apache.hadoop.hbase.util.Bytes;
035import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
036import org.apache.hadoop.hbase.util.Threads;
037import org.junit.jupiter.api.AfterAll;
038import org.junit.jupiter.api.AfterEach;
039import org.junit.jupiter.api.BeforeEach;
040import org.junit.jupiter.api.Test;
041import org.junit.jupiter.api.TestInfo;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045/**
046 * Test to verify that the cloned table is independent of the table from which it was cloned
047 */
048public abstract class SnapshotCloneIndependenceTestBase {
049
050  private static final Logger LOG =
051    LoggerFactory.getLogger(SnapshotCloneIndependenceTestBase.class);
052
053  protected static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
054
055  protected static final int NUM_RS = 2;
056  private static final String TEST_FAM_STR = "fam";
057  protected static final byte[] TEST_FAM = Bytes.toBytes(TEST_FAM_STR);
058  private static final int CLEANER_INTERVAL = 100;
059
060  private FileSystem fs;
061  private Path rootDir;
062  private Admin admin;
063  private TableName originalTableName;
064  private Table originalTable;
065  private TableName cloneTableName;
066  private int countOriginalTable;
067  String snapshotNameAsString;
068  String snapshotName;
069
070  protected static void setupConf(Configuration conf) {
071    // Up the handlers; this test needs more than usual.
072    conf.setInt(HConstants.REGION_SERVER_HIGH_PRIORITY_HANDLER_COUNT, 15);
073    // enable snapshot support
074    conf.setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true);
075    // change the flush size to a small amount, regulating number of store files
076    conf.setInt("hbase.hregion.memstore.flush.size", 25000);
077    // so make sure we get a compaction when doing a load, but keep around
078    // some files in the store
079    conf.setInt("hbase.hstore.compaction.min", 10);
080    conf.setInt("hbase.hstore.compactionThreshold", 10);
081    // block writes if we get to 12 store files
082    conf.setInt("hbase.hstore.blockingStoreFiles", 12);
083    conf.setInt("hbase.regionserver.msginterval", 100);
084    conf.setBoolean("hbase.master.enabletable.roundrobin", true);
085    // Avoid potentially aggressive splitting which would cause snapshot to fail
086    conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY,
087      ConstantSizeRegionSplitPolicy.class.getName());
088    // Execute cleaner frequently to induce failures
089    conf.setInt("hbase.master.cleaner.interval", CLEANER_INTERVAL);
090    conf.setInt("hbase.master.hfilecleaner.plugins.snapshot.period", CLEANER_INTERVAL);
091    // Effectively disable TimeToLiveHFileCleaner. Don't want to fully disable it because that
092    // will even trigger races between creating the directory containing back references and
093    // the back reference itself.
094    conf.setInt("hbase.master.hfilecleaner.ttl", CLEANER_INTERVAL);
095  }
096
097  @BeforeEach
098  public void setup(TestInfo testInfo) throws Exception {
099    fs = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getFileSystem();
100    rootDir = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir();
101
102    admin = UTIL.getAdmin();
103    originalTableName = TableName.valueOf("test" + testInfo.getTestMethod().get().getName());
104    cloneTableName = TableName.valueOf("test-clone-" + originalTableName);
105    snapshotNameAsString = "snapshot_" + originalTableName;
106    snapshotName = snapshotNameAsString;
107
108    originalTable = createTable(originalTableName, TEST_FAM);
109    loadData(originalTable, TEST_FAM);
110    countOriginalTable = countRows(originalTable);
111    LOG.info("Original table has: " + countOriginalTable + " rows");
112  }
113
114  @AfterEach
115  public void tearDown() throws Exception {
116    UTIL.deleteTable(originalTableName);
117    UTIL.deleteTable(cloneTableName);
118    SnapshotTestingUtils.deleteAllSnapshots(UTIL.getAdmin());
119    SnapshotTestingUtils.deleteArchiveDirectory(UTIL);
120  }
121
122  @AfterAll
123  public static void cleanupTest() throws Exception {
124    try {
125      UTIL.shutdownMiniCluster();
126    } catch (Exception e) {
127      LOG.warn("failure shutting down cluster", e);
128    }
129  }
130
131  /**
132   * Verify that adding data to the cloned table will not affect the original, and vice-versa when
133   * it is taken as an online snapshot.
134   */
135  @Test
136  public void testOnlineSnapshotAppendIndependent() throws Exception {
137    createAndCloneSnapshot(true);
138    runTestSnapshotAppendIndependent();
139  }
140
141  /**
142   * Verify that adding data to the cloned table will not affect the original, and vice-versa when
143   * it is taken as an offline snapshot.
144   */
145  @Test
146  public void testOfflineSnapshotAppendIndependent() throws Exception {
147    createAndCloneSnapshot(false);
148    runTestSnapshotAppendIndependent();
149  }
150
151  /**
152   * Verify that adding metadata to the cloned table will not affect the original, and vice-versa
153   * when it is taken as an online snapshot.
154   */
155  @Test
156  public void testOnlineSnapshotMetadataChangesIndependent() throws Exception {
157    createAndCloneSnapshot(true);
158    runTestSnapshotMetadataChangesIndependent();
159  }
160
161  /**
162   * Verify that adding netadata to the cloned table will not affect the original, and vice-versa
163   * when is taken as an online snapshot.
164   */
165  @Test
166  public void testOfflineSnapshotMetadataChangesIndependent() throws Exception {
167    createAndCloneSnapshot(false);
168    runTestSnapshotMetadataChangesIndependent();
169  }
170
171  /**
172   * Verify that region operations, in this case splitting a region, are independent between the
173   * cloned table and the original.
174   */
175  @Test
176  public void testOfflineSnapshotRegionOperationsIndependent() throws Exception {
177    createAndCloneSnapshot(false);
178    runTestRegionOperationsIndependent();
179  }
180
181  /**
182   * Verify that region operations, in this case splitting a region, are independent between the
183   * cloned table and the original.
184   */
185  @Test
186  public void testOnlineSnapshotRegionOperationsIndependent() throws Exception {
187    createAndCloneSnapshot(true);
188    runTestRegionOperationsIndependent();
189  }
190
191  @Test
192  public void testOfflineSnapshotDeleteIndependent() throws Exception {
193    createAndCloneSnapshot(false);
194    runTestSnapshotDeleteIndependent();
195  }
196
197  @Test
198  public void testOnlineSnapshotDeleteIndependent() throws Exception {
199    createAndCloneSnapshot(true);
200    runTestSnapshotDeleteIndependent();
201  }
202
203  private static void waitOnSplit(Connection c, final Table t, int originalCount) throws Exception {
204    for (int i = 0; i < 200; i++) {
205      Threads.sleepWithoutInterrupt(500);
206      try (RegionLocator locator = c.getRegionLocator(t.getName())) {
207        if (locator.getAllRegionLocations().size() > originalCount) {
208          return;
209        }
210      }
211    }
212    throw new Exception("Split did not increase the number of regions");
213  }
214
215  /**
216   * Takes the snapshot of originalTable and clones the snapshot to another tables. If
217   * {@code online} is false, the original table is disabled during taking snapshot, so also enables
218   * it again.
219   * @param online - Whether the table is online or not during the snapshot
220   */
221  private void createAndCloneSnapshot(boolean online) throws Exception {
222    SnapshotTestingUtils.createSnapshotAndValidate(admin, originalTableName, TEST_FAM_STR,
223      snapshotNameAsString, rootDir, fs, online);
224
225    // If offline, enable the table disabled by snapshot testing util.
226    if (!online) {
227      admin.enableTable(originalTableName);
228      UTIL.waitTableAvailable(originalTableName);
229    }
230
231    admin.cloneSnapshot(snapshotName, cloneTableName);
232    UTIL.waitUntilAllRegionsAssigned(cloneTableName);
233  }
234
235  /**
236   * Verify that adding data to original table or clone table doesn't affect other table.
237   */
238  private void runTestSnapshotAppendIndependent() throws Exception {
239    try (Table clonedTable = UTIL.getConnection().getTable(cloneTableName)) {
240      final int clonedTableRowCount = countRows(clonedTable);
241
242      assertEquals(countOriginalTable, clonedTableRowCount,
243        "The line counts of original and cloned tables do not match after clone. ");
244
245      // Attempt to add data to the test
246      Put p = new Put(Bytes.toBytes("new-row-" + EnvironmentEdgeManager.currentTime()));
247      p.addColumn(TEST_FAM, Bytes.toBytes("someQualifier"), Bytes.toBytes("someString"));
248      originalTable.put(p);
249
250      // Verify that the new row is not in the restored table
251      assertEquals(countOriginalTable + 1, countRows(originalTable),
252        "The row count of the original table was not modified by the put");
253      assertEquals(clonedTableRowCount, countRows(clonedTable),
254        "The row count of the cloned table changed as a result of addition to the original");
255
256      Put p2 = new Put(Bytes.toBytes("new-row-" + EnvironmentEdgeManager.currentTime()));
257      p2.addColumn(TEST_FAM, Bytes.toBytes("someQualifier"), Bytes.toBytes("someString"));
258      clonedTable.put(p2);
259
260      // Verify that the row is not added to the original table.
261      assertEquals(countOriginalTable + 1, countRows(originalTable),
262        "The row count of the original table was modified by the put to the clone");
263      assertEquals(clonedTableRowCount + 1, countRows(clonedTable),
264        "The row count of the cloned table was not modified by the put");
265    }
266  }
267
268  /**
269   * Do a split, and verify that this only affects one table
270   */
271  private void runTestRegionOperationsIndependent() throws Exception {
272    // Verify that region information is the same pre-split
273    UTIL.getConnection().clearRegionLocationCache();
274    List<RegionInfo> originalTableHRegions = admin.getRegions(originalTableName);
275
276    final int originalRegionCount = originalTableHRegions.size();
277    final int cloneTableRegionCount = admin.getRegions(cloneTableName).size();
278    assertEquals(originalRegionCount, cloneTableRegionCount,
279      "The number of regions in the cloned table is different than in the original table.");
280
281    // Split a region on the parent table
282    admin.splitRegionAsync(originalTableHRegions.get(0).getRegionName()).get();
283    waitOnSplit(UTIL.getConnection(), originalTable, originalRegionCount);
284
285    // Verify that the cloned table region is not split
286    final int cloneTableRegionCount2 = admin.getRegions(cloneTableName).size();
287    assertEquals(cloneTableRegionCount, cloneTableRegionCount2,
288      "The number of regions in the cloned table changed though none of its regions were split.");
289  }
290
291  /**
292   * Add metadata, and verify that this only affects one table
293   */
294  private void runTestSnapshotMetadataChangesIndependent() throws Exception {
295    // Add a new column family to the original table
296    byte[] TEST_FAM_2 = Bytes.toBytes("fam2");
297    ColumnFamilyDescriptor familyDescriptor = ColumnFamilyDescriptorBuilder.of(TEST_FAM_2);
298
299    admin.disableTable(originalTableName);
300    admin.addColumnFamily(originalTableName, familyDescriptor);
301
302    // Verify that it is not in the snapshot
303    admin.enableTable(originalTableName);
304    UTIL.waitTableAvailable(originalTableName);
305
306    // get a description of the cloned table
307    // get a list of its families
308    // assert that the family is there
309    TableDescriptor originalTableDescriptor = originalTable.getDescriptor();
310    TableDescriptor clonedTableDescriptor = admin.getDescriptor(cloneTableName);
311
312    assertTrue(originalTableDescriptor.hasColumnFamily(TEST_FAM),
313      "The original family was not found. There is something wrong. ");
314    assertTrue(clonedTableDescriptor.hasColumnFamily(TEST_FAM),
315      "The original family was not found in the clone. There is something wrong. ");
316
317    assertTrue(originalTableDescriptor.hasColumnFamily(TEST_FAM_2),
318      "The new family was not found. ");
319    assertTrue(!clonedTableDescriptor.hasColumnFamily(TEST_FAM_2),
320      "The new family was not found. ");
321  }
322
323  /**
324   * Verify that deleting the snapshot does not affect either table.
325   */
326  private void runTestSnapshotDeleteIndependent() throws Exception {
327    // Ensure the original table does not reference the HFiles anymore
328    admin.majorCompact(originalTableName);
329
330    // Deleting the snapshot used to break the cloned table by deleting in-use HFiles
331    admin.deleteSnapshot(snapshotName);
332
333    // Wait for cleaner run and DFS heartbeats so that anything that is deletable is fully deleted
334    Pattern pattern = Pattern.compile(snapshotNameAsString);
335    do {
336      Thread.sleep(5000);
337    } while (!admin.listSnapshots(pattern).isEmpty());
338
339    try (Table original = UTIL.getConnection().getTable(originalTableName)) {
340      try (Table clonedTable = UTIL.getConnection().getTable(cloneTableName)) {
341        // Verify that all regions of both tables are readable
342        final int origTableRowCount = countRows(original);
343        final int clonedTableRowCount = countRows(clonedTable);
344        assertEquals(origTableRowCount, clonedTableRowCount);
345      }
346    }
347  }
348
349  protected Table createTable(final TableName table, byte[] family) throws Exception {
350    Table t = UTIL.createTable(table, family);
351    // Wait for everything to be ready with the table
352    UTIL.waitUntilAllRegionsAssigned(table);
353
354    // At this point the table should be good to go.
355    return t;
356  }
357
358  public void loadData(final Table table, byte[]... families) throws Exception {
359    UTIL.loadTable(originalTable, TEST_FAM);
360  }
361
362  protected int countRows(final Table table, final byte[]... families) throws Exception {
363    return UTIL.countRows(table, families);
364  }
365}