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}