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