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}