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.master.procedure; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertTrue; 022import static org.junit.Assert.fail; 023 024import java.io.IOException; 025import java.util.Arrays; 026import java.util.stream.Collectors; 027 028import org.apache.hadoop.conf.Configuration; 029import org.apache.hadoop.fs.FileSystem; 030import org.apache.hadoop.fs.Path; 031import org.apache.hadoop.hbase.HBaseClassTestRule; 032import org.apache.hadoop.hbase.HBaseIOException; 033import org.apache.hadoop.hbase.TableName; 034import org.apache.hadoop.hbase.TableNotDisabledException; 035import org.apache.hadoop.hbase.TableNotFoundException; 036import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 037import org.apache.hadoop.hbase.client.RegionInfo; 038import org.apache.hadoop.hbase.client.TableDescriptor; 039import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 040import org.apache.hadoop.hbase.master.MasterFileSystem; 041import org.apache.hadoop.hbase.procedure2.Procedure; 042import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; 043import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility; 044import org.apache.hadoop.hbase.testclassification.MasterTests; 045import org.apache.hadoop.hbase.testclassification.MediumTests; 046import org.apache.hadoop.hbase.util.Bytes; 047import org.apache.hadoop.hbase.util.FSUtils; 048import org.apache.hadoop.hbase.util.ModifyRegionUtils; 049import org.junit.ClassRule; 050import org.junit.Rule; 051import org.junit.Test; 052import org.junit.experimental.categories.Category; 053import org.junit.rules.TestName; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos; 057 058@Category({ MasterTests.class, MediumTests.class }) 059public class TestTruncateTableProcedure extends TestTableDDLProcedureBase { 060 061 @ClassRule 062 public static final HBaseClassTestRule CLASS_RULE = 063 HBaseClassTestRule.forClass(TestTruncateTableProcedure.class); 064 065 private static final Logger LOG = LoggerFactory.getLogger(TestTruncateTableProcedure.class); 066 067 @Rule 068 public TestName name = new TestName(); 069 070 @Test 071 public void testTruncateNotExistentTable() throws Exception { 072 final TableName tableName = TableName.valueOf(name.getMethodName()); 073 074 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 075 // HBASE-20178 has us fail-fast, in the constructor, so add try/catch for this case. 076 // Keep old way of looking at procedure too. 077 Throwable cause = null; 078 try { 079 long procId = ProcedureTestingUtility.submitAndWait(procExec, 080 new TruncateTableProcedure(procExec.getEnvironment(), tableName, true)); 081 082 // Second delete should fail with TableNotFound 083 Procedure<?> result = procExec.getResult(procId); 084 assertTrue(result.isFailed()); 085 cause = ProcedureTestingUtility.getExceptionCause(result); 086 } catch (Throwable t) { 087 cause = t; 088 } 089 LOG.debug("Truncate failed with exception: " + cause); 090 assertTrue(cause instanceof TableNotFoundException); 091 } 092 093 @Test 094 public void testTruncateNotDisabledTable() throws Exception { 095 final TableName tableName = TableName.valueOf(name.getMethodName()); 096 097 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 098 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "f"); 099 100 // HBASE-20178 has us fail-fast, in the constructor, so add try/catch for this case. 101 // Keep old way of looking at procedure too. 102 Throwable cause = null; 103 try { 104 long procId = ProcedureTestingUtility.submitAndWait(procExec, 105 new TruncateTableProcedure(procExec.getEnvironment(), tableName, false)); 106 107 // Second delete should fail with TableNotDisabled 108 Procedure<?> result = procExec.getResult(procId); 109 assertTrue(result.isFailed()); 110 cause = ProcedureTestingUtility.getExceptionCause(result); 111 } catch (Throwable t) { 112 cause = t; 113 } 114 LOG.debug("Truncate failed with exception: " + cause); 115 assertTrue(cause instanceof TableNotDisabledException); 116 } 117 118 @Test 119 public void testSimpleTruncatePreserveSplits() throws Exception { 120 final TableName tableName = TableName.valueOf(name.getMethodName()); 121 testSimpleTruncate(tableName, true); 122 } 123 124 @Test 125 public void testSimpleTruncateNoPreserveSplits() throws Exception { 126 final TableName tableName = TableName.valueOf(name.getMethodName()); 127 testSimpleTruncate(tableName, false); 128 } 129 130 private void testSimpleTruncate(final TableName tableName, final boolean preserveSplits) 131 throws Exception { 132 final String[] families = new String[] { "f1", "f2" }; 133 final byte[][] splitKeys = new byte[][] { 134 Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") 135 }; 136 137 RegionInfo[] regions = MasterProcedureTestingUtility.createTable( 138 getMasterProcedureExecutor(), tableName, splitKeys, families); 139 // load and verify that there are rows in the table 140 MasterProcedureTestingUtility.loadData( 141 UTIL.getConnection(), tableName, 100, splitKeys, families); 142 assertEquals(100, UTIL.countRows(tableName)); 143 // disable the table 144 UTIL.getAdmin().disableTable(tableName); 145 146 // truncate the table 147 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 148 long procId = ProcedureTestingUtility.submitAndWait(procExec, 149 new TruncateTableProcedure(procExec.getEnvironment(), tableName, preserveSplits)); 150 ProcedureTestingUtility.assertProcNotFailed(procExec, procId); 151 152 // If truncate procedure completed successfully, it means all regions were assigned correctly 153 // and table is enabled now. 154 UTIL.waitUntilAllRegionsAssigned(tableName); 155 156 // validate the table regions and layout 157 regions = UTIL.getAdmin().getRegions(tableName).toArray(new RegionInfo[0]); 158 if (preserveSplits) { 159 assertEquals(1 + splitKeys.length, regions.length); 160 } else { 161 assertEquals(1, regions.length); 162 } 163 MasterProcedureTestingUtility.validateTableCreation( 164 UTIL.getHBaseCluster().getMaster(), tableName, regions, families); 165 166 // verify that there are no rows in the table 167 assertEquals(0, UTIL.countRows(tableName)); 168 169 // verify that the table is read/writable 170 MasterProcedureTestingUtility.loadData( 171 UTIL.getConnection(), tableName, 50, splitKeys, families); 172 assertEquals(50, UTIL.countRows(tableName)); 173 } 174 175 @Test 176 public void testRecoveryAndDoubleExecutionPreserveSplits() throws Exception { 177 final TableName tableName = TableName.valueOf(name.getMethodName()); 178 testRecoveryAndDoubleExecution(tableName, true); 179 } 180 181 @Test 182 public void testRecoveryAndDoubleExecutionNoPreserveSplits() throws Exception { 183 final TableName tableName = TableName.valueOf(name.getMethodName()); 184 testRecoveryAndDoubleExecution(tableName, false); 185 } 186 187 private void testRecoveryAndDoubleExecution(final TableName tableName, 188 final boolean preserveSplits) throws Exception { 189 final String[] families = new String[] { "f1", "f2" }; 190 191 // create the table 192 final byte[][] splitKeys = new byte[][] { 193 Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") 194 }; 195 RegionInfo[] regions = MasterProcedureTestingUtility.createTable( 196 getMasterProcedureExecutor(), tableName, splitKeys, families); 197 // load and verify that there are rows in the table 198 MasterProcedureTestingUtility.loadData( 199 UTIL.getConnection(), tableName, 100, splitKeys, families); 200 assertEquals(100, UTIL.countRows(tableName)); 201 // disable the table 202 UTIL.getAdmin().disableTable(tableName); 203 204 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 205 ProcedureTestingUtility.waitNoProcedureRunning(procExec); 206 ProcedureTestingUtility.setKillIfHasParent(procExec, false); 207 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true); 208 209 // Start the Truncate procedure && kill the executor 210 long procId = procExec.submitProcedure( 211 new TruncateTableProcedure(procExec.getEnvironment(), tableName, preserveSplits)); 212 213 // Restart the executor and execute the step twice 214 MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId); 215 216 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, false); 217 UTIL.waitUntilAllRegionsAssigned(tableName); 218 219 // validate the table regions and layout 220 regions = UTIL.getAdmin().getRegions(tableName).toArray(new RegionInfo[0]); 221 if (preserveSplits) { 222 assertEquals(1 + splitKeys.length, regions.length); 223 } else { 224 assertEquals(1, regions.length); 225 } 226 MasterProcedureTestingUtility.validateTableCreation( 227 UTIL.getHBaseCluster().getMaster(), tableName, regions, families); 228 229 // verify that there are no rows in the table 230 assertEquals(0, UTIL.countRows(tableName)); 231 232 // verify that the table is read/writable 233 MasterProcedureTestingUtility.loadData( 234 UTIL.getConnection(), tableName, 50, splitKeys, families); 235 assertEquals(50, UTIL.countRows(tableName)); 236 } 237 238 @Test 239 public void testOnHDFSFailurePreserveSplits() throws Exception { 240 final TableName tableName = TableName.valueOf(name.getMethodName()); 241 testOnHDFSFailure(tableName, true); 242 } 243 244 @Test 245 public void testOnHDFSFailureNoPreserveSplits() throws Exception { 246 final TableName tableName = TableName.valueOf(name.getMethodName()); 247 testOnHDFSFailure(tableName, false); 248 } 249 250 public static class TruncateTableProcedureOnHDFSFailure extends TruncateTableProcedure { 251 252 private boolean failOnce = false; 253 254 public TruncateTableProcedureOnHDFSFailure() { 255 // Required by the Procedure framework to create the procedure on replay 256 super(); 257 } 258 259 public TruncateTableProcedureOnHDFSFailure(final MasterProcedureEnv env, TableName tableName, 260 boolean preserveSplits) 261 throws HBaseIOException { 262 super(env, tableName, preserveSplits); 263 } 264 265 @Override 266 protected Flow executeFromState(MasterProcedureEnv env, 267 MasterProcedureProtos.TruncateTableState state) throws InterruptedException { 268 269 if (!failOnce && 270 state == MasterProcedureProtos.TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT) { 271 try { 272 // To emulate an HDFS failure, create only the first region directory 273 RegionInfo regionInfo = getFirstRegionInfo(); 274 Configuration conf = env.getMasterConfiguration(); 275 MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem(); 276 Path tempdir = mfs.getTempDir(); 277 Path tableDir = FSUtils.getTableDir(tempdir, regionInfo.getTable()); 278 Path regionDir = FSUtils.getRegionDirFromTableDir(tableDir, regionInfo); 279 FileSystem fs = FileSystem.get(conf); 280 fs.mkdirs(regionDir); 281 282 failOnce = true; 283 return Flow.HAS_MORE_STATE; 284 } catch (IOException e) { 285 fail("failed to create a region directory: " + e); 286 } 287 } 288 289 return super.executeFromState(env, state); 290 } 291 } 292 293 private void testOnHDFSFailure(TableName tableName, boolean preserveSplits) throws Exception { 294 String[] families = new String[] { "f1", "f2" }; 295 byte[][] splitKeys = new byte[][] { 296 Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") 297 }; 298 299 // create a table 300 MasterProcedureTestingUtility.createTable( 301 getMasterProcedureExecutor(), tableName, splitKeys, families); 302 303 // load and verify that there are rows in the table 304 MasterProcedureTestingUtility.loadData( 305 UTIL.getConnection(), tableName, 100, splitKeys, families); 306 assertEquals(100, UTIL.countRows(tableName)); 307 308 // disable the table 309 UTIL.getAdmin().disableTable(tableName); 310 311 // truncate the table 312 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 313 long procId = ProcedureTestingUtility.submitAndWait(procExec, 314 new TruncateTableProcedureOnHDFSFailure(procExec.getEnvironment(), tableName, 315 preserveSplits)); 316 ProcedureTestingUtility.assertProcNotFailed(procExec, procId); 317 } 318 319 @Test 320 public void testTruncateWithPreserveAfterSplit() throws Exception { 321 String[] families = new String[] { "f1", "f2" }; 322 byte[][] splitKeys = 323 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 324 TableName tableName = TableName.valueOf(name.getMethodName()); 325 RegionInfo[] regions = MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), 326 tableName, splitKeys, families); 327 splitAndTruncate(tableName, regions, 1); 328 } 329 330 @Test 331 public void testTruncatePreserveWithReplicaRegionAfterSplit() throws Exception { 332 String[] families = new String[] { "f1", "f2" }; 333 byte[][] splitKeys = 334 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 335 TableName tableName = TableName.valueOf(name.getMethodName()); 336 337 // create a table with region replications 338 TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName).setRegionReplication(3) 339 .setColumnFamilies(Arrays.stream(families) 340 .map(fam -> ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(fam)).build()) 341 .collect(Collectors.toList())) 342 .build(); 343 RegionInfo[] regions = ModifyRegionUtils.createRegionInfos(htd, splitKeys); 344 ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 345 long procId = ProcedureTestingUtility.submitAndWait(procExec, 346 new CreateTableProcedure(procExec.getEnvironment(), htd, regions)); 347 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId)); 348 349 splitAndTruncate(tableName, regions, 3); 350 } 351 352 private void splitAndTruncate(TableName tableName, RegionInfo[] regions, int regionReplication) 353 throws IOException, InterruptedException { 354 // split a region 355 UTIL.getAdmin().split(tableName, new byte[] { '0' }); 356 357 // wait until split really happens 358 UTIL.waitFor(60000, 359 () -> UTIL.getAdmin().getRegions(tableName).size() > regions.length * regionReplication); 360 361 // disable the table 362 UTIL.getAdmin().disableTable(tableName); 363 364 // truncate the table 365 ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 366 long procId = ProcedureTestingUtility.submitAndWait(procExec, 367 new TruncateTableProcedure(procExec.getEnvironment(), tableName, true)); 368 ProcedureTestingUtility.assertProcNotFailed(procExec, procId); 369 370 UTIL.waitUntilAllRegionsAssigned(tableName); 371 // confirm that we have the correct number of regions 372 assertEquals((regions.length + 1) * regionReplication, 373 UTIL.getAdmin().getRegions(tableName).size()); 374 } 375}