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.MasterTests; 044import org.apache.hadoop.hbase.testclassification.MediumTests; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.apache.hadoop.hbase.util.FSUtils; 047import org.apache.hadoop.hbase.util.ModifyRegionUtils; 048import org.junit.ClassRule; 049import org.junit.Rule; 050import org.junit.Test; 051import org.junit.experimental.categories.Category; 052import org.junit.rules.TestName; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 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().getTableRegions(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.setKillAndToggleBeforeStoreUpdate(procExec, true); 207 208 // Start the Truncate procedure && kill the executor 209 long procId = procExec.submitProcedure( 210 new TruncateTableProcedure(procExec.getEnvironment(), tableName, preserveSplits)); 211 212 // Restart the executor and execute the step twice 213 MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId); 214 215 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, false); 216 UTIL.waitUntilAllRegionsAssigned(tableName); 217 218 // validate the table regions and layout 219 regions = UTIL.getAdmin().getTableRegions(tableName).toArray(new RegionInfo[0]); 220 if (preserveSplits) { 221 assertEquals(1 + splitKeys.length, regions.length); 222 } else { 223 assertEquals(1, regions.length); 224 } 225 MasterProcedureTestingUtility.validateTableCreation( 226 UTIL.getHBaseCluster().getMaster(), tableName, regions, families); 227 228 // verify that there are no rows in the table 229 assertEquals(0, UTIL.countRows(tableName)); 230 231 // verify that the table is read/writable 232 MasterProcedureTestingUtility.loadData( 233 UTIL.getConnection(), tableName, 50, splitKeys, families); 234 assertEquals(50, UTIL.countRows(tableName)); 235 } 236 237 @Test 238 public void testOnHDFSFailurePreserveSplits() throws Exception { 239 final TableName tableName = TableName.valueOf(name.getMethodName()); 240 testOnHDFSFailure(tableName, true); 241 } 242 243 @Test 244 public void testOnHDFSFailureNoPreserveSplits() throws Exception { 245 final TableName tableName = TableName.valueOf(name.getMethodName()); 246 testOnHDFSFailure(tableName, false); 247 } 248 249 public static class TruncateTableProcedureOnHDFSFailure extends TruncateTableProcedure { 250 251 private boolean failOnce = false; 252 253 public TruncateTableProcedureOnHDFSFailure() { 254 // Required by the Procedure framework to create the procedure on replay 255 super(); 256 } 257 258 public TruncateTableProcedureOnHDFSFailure(final MasterProcedureEnv env, TableName tableName, 259 boolean preserveSplits) 260 throws HBaseIOException { 261 super(env, tableName, preserveSplits); 262 } 263 264 @Override 265 protected Flow executeFromState(MasterProcedureEnv env, 266 MasterProcedureProtos.TruncateTableState state) throws InterruptedException { 267 268 if (!failOnce && 269 state == MasterProcedureProtos.TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT) { 270 try { 271 // To emulate an HDFS failure, create only the first region directory 272 RegionInfo regionInfo = getFirstRegionInfo(); 273 Configuration conf = env.getMasterConfiguration(); 274 MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem(); 275 Path tempdir = mfs.getTempDir(); 276 Path tableDir = FSUtils.getTableDir(tempdir, regionInfo.getTable()); 277 Path regionDir = FSUtils.getRegionDirFromTableDir(tableDir, regionInfo); 278 FileSystem fs = FileSystem.get(conf); 279 fs.mkdirs(regionDir); 280 281 failOnce = true; 282 return Flow.HAS_MORE_STATE; 283 } catch (IOException e) { 284 fail("failed to create a region directory: " + e); 285 } 286 } 287 288 return super.executeFromState(env, state); 289 } 290 } 291 292 private void testOnHDFSFailure(TableName tableName, boolean preserveSplits) throws Exception { 293 String[] families = new String[] { "f1", "f2" }; 294 byte[][] splitKeys = new byte[][] { 295 Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") 296 }; 297 298 // create a table 299 MasterProcedureTestingUtility.createTable( 300 getMasterProcedureExecutor(), tableName, splitKeys, families); 301 302 // load and verify that there are rows in the table 303 MasterProcedureTestingUtility.loadData( 304 UTIL.getConnection(), tableName, 100, splitKeys, families); 305 assertEquals(100, UTIL.countRows(tableName)); 306 307 // disable the table 308 UTIL.getAdmin().disableTable(tableName); 309 310 // truncate the table 311 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 312 long procId = ProcedureTestingUtility.submitAndWait(procExec, 313 new TruncateTableProcedureOnHDFSFailure(procExec.getEnvironment(), tableName, 314 preserveSplits)); 315 ProcedureTestingUtility.assertProcNotFailed(procExec, procId); 316 } 317 318 @Test 319 public void testTruncateWithPreserveAfterSplit() throws Exception { 320 String[] families = new String[] { "f1", "f2" }; 321 byte[][] splitKeys = 322 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 323 TableName tableName = TableName.valueOf(name.getMethodName()); 324 RegionInfo[] regions = MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), 325 tableName, splitKeys, families); 326 splitAndTruncate(tableName, regions, 1); 327 } 328 329 @Test 330 public void testTruncatePreserveWithReplicaRegionAfterSplit() throws Exception { 331 String[] families = new String[] { "f1", "f2" }; 332 byte[][] splitKeys = 333 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 334 TableName tableName = TableName.valueOf(name.getMethodName()); 335 336 // create a table with region replications 337 TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName).setRegionReplication(3) 338 .setColumnFamilies(Arrays.stream(families) 339 .map(fam -> ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(fam)).build()) 340 .collect(Collectors.toList())) 341 .build(); 342 RegionInfo[] regions = ModifyRegionUtils.createRegionInfos(htd, splitKeys); 343 ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 344 long procId = ProcedureTestingUtility.submitAndWait(procExec, 345 new CreateTableProcedure(procExec.getEnvironment(), htd, regions)); 346 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId)); 347 348 splitAndTruncate(tableName, regions, 3); 349 } 350 351 private void splitAndTruncate(TableName tableName, RegionInfo[] regions, int regionReplication) 352 throws IOException, InterruptedException { 353 // split a region 354 UTIL.getAdmin().split(tableName, new byte[] { '0' }); 355 356 // wait until split really happens 357 UTIL.waitFor(60000, 358 () -> UTIL.getAdmin().getRegions(tableName).size() > regions.length * regionReplication); 359 360 // disable the table 361 UTIL.getAdmin().disableTable(tableName); 362 363 // truncate the table 364 ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 365 long procId = ProcedureTestingUtility.submitAndWait(procExec, 366 new TruncateTableProcedure(procExec.getEnvironment(), tableName, true)); 367 ProcedureTestingUtility.assertProcNotFailed(procExec, procId); 368 369 UTIL.waitUntilAllRegionsAssigned(tableName); 370 // confirm that we have the correct number of regions 371 assertEquals((regions.length + 1) * regionReplication, 372 UTIL.getAdmin().getRegions(tableName).size()); 373 } 374}