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