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 = 135 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 136 137 RegionInfo[] regions = MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), 138 tableName, splitKeys, families); 139 // load and verify that there are rows in the table 140 MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, splitKeys, 141 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(UTIL.getHBaseCluster().getMaster(), 164 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(UTIL.getConnection(), tableName, 50, splitKeys, 171 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 = 193 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 194 RegionInfo[] regions = MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), 195 tableName, splitKeys, families); 196 // load and verify that there are rows in the table 197 MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, splitKeys, 198 families); 199 assertEquals(100, UTIL.countRows(tableName)); 200 // disable the table 201 UTIL.getAdmin().disableTable(tableName); 202 203 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 204 ProcedureTestingUtility.waitNoProcedureRunning(procExec); 205 ProcedureTestingUtility.setKillIfHasParent(procExec, false); 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().getRegions(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(UTIL.getHBaseCluster().getMaster(), 226 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(UTIL.getConnection(), tableName, 50, splitKeys, 233 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) throws HBaseIOException { 260 super(env, tableName, preserveSplits); 261 } 262 263 @Override 264 protected Flow executeFromState(MasterProcedureEnv env, 265 MasterProcedureProtos.TruncateTableState state) throws InterruptedException { 266 267 if ( 268 !failOnce 269 && state == MasterProcedureProtos.TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT 270 ) { 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 = CommonFSUtils.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 = 296 new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") }; 297 298 // create a table 299 MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), tableName, splitKeys, 300 families); 301 302 // load and verify that there are rows in the table 303 MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, splitKeys, 304 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 = 313 ProcedureTestingUtility.submitAndWait(procExec, new TruncateTableProcedureOnHDFSFailure( 314 procExec.getEnvironment(), tableName, 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}