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