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;
027
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileSystem;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.hbase.HBaseClassTestRule;
032import org.apache.hadoop.hbase.HBaseIOException;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.TableNotDisabledException;
035import org.apache.hadoop.hbase.TableNotFoundException;
036import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
037import org.apache.hadoop.hbase.client.RegionInfo;
038import org.apache.hadoop.hbase.client.TableDescriptor;
039import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
040import org.apache.hadoop.hbase.master.MasterFileSystem;
041import org.apache.hadoop.hbase.procedure2.Procedure;
042import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
043import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
044import org.apache.hadoop.hbase.testclassification.MasterTests;
045import org.apache.hadoop.hbase.testclassification.MediumTests;
046import org.apache.hadoop.hbase.util.Bytes;
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;
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().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(
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.setKillIfHasParent(procExec, false);
207    ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true);
208
209    // Start the Truncate procedure && kill the executor
210    long procId = procExec.submitProcedure(
211      new TruncateTableProcedure(procExec.getEnvironment(), tableName, preserveSplits));
212
213    // Restart the executor and execute the step twice
214    MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId);
215
216    ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, false);
217    UTIL.waitUntilAllRegionsAssigned(tableName);
218
219    // validate the table regions and layout
220    regions = UTIL.getAdmin().getRegions(tableName).toArray(new RegionInfo[0]);
221    if (preserveSplits) {
222      assertEquals(1 + splitKeys.length, regions.length);
223    } else {
224      assertEquals(1, regions.length);
225    }
226    MasterProcedureTestingUtility.validateTableCreation(
227      UTIL.getHBaseCluster().getMaster(), tableName, regions, families);
228
229    // verify that there are no rows in the table
230    assertEquals(0, UTIL.countRows(tableName));
231
232    // verify that the table is read/writable
233    MasterProcedureTestingUtility.loadData(
234      UTIL.getConnection(), tableName, 50, splitKeys, families);
235    assertEquals(50, UTIL.countRows(tableName));
236  }
237
238  @Test
239  public void testOnHDFSFailurePreserveSplits() throws Exception {
240    final TableName tableName = TableName.valueOf(name.getMethodName());
241    testOnHDFSFailure(tableName, true);
242  }
243
244  @Test
245  public void testOnHDFSFailureNoPreserveSplits() throws Exception {
246    final TableName tableName = TableName.valueOf(name.getMethodName());
247    testOnHDFSFailure(tableName, false);
248  }
249
250  public static class TruncateTableProcedureOnHDFSFailure extends TruncateTableProcedure {
251
252    private boolean failOnce = false;
253
254    public TruncateTableProcedureOnHDFSFailure() {
255      // Required by the Procedure framework to create the procedure on replay
256      super();
257    }
258
259    public TruncateTableProcedureOnHDFSFailure(final MasterProcedureEnv env, TableName tableName,
260      boolean preserveSplits)
261      throws HBaseIOException {
262      super(env, tableName, preserveSplits);
263    }
264
265    @Override
266    protected Flow executeFromState(MasterProcedureEnv env,
267      MasterProcedureProtos.TruncateTableState state) throws InterruptedException {
268
269      if (!failOnce &&
270        state == MasterProcedureProtos.TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT) {
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 = FSUtils.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 = new byte[][] {
296      Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c")
297    };
298
299    // create a table
300    MasterProcedureTestingUtility.createTable(
301      getMasterProcedureExecutor(), tableName, splitKeys, families);
302
303    // load and verify that there are rows in the table
304    MasterProcedureTestingUtility.loadData(
305      UTIL.getConnection(), tableName, 100, splitKeys, families);
306    assertEquals(100, UTIL.countRows(tableName));
307
308    // disable the table
309    UTIL.getAdmin().disableTable(tableName);
310
311    // truncate the table
312    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
313    long procId = ProcedureTestingUtility.submitAndWait(procExec,
314      new TruncateTableProcedureOnHDFSFailure(procExec.getEnvironment(), tableName,
315        preserveSplits));
316    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
317  }
318
319  @Test
320  public void testTruncateWithPreserveAfterSplit() throws Exception {
321    String[] families = new String[] { "f1", "f2" };
322    byte[][] splitKeys =
323      new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") };
324    TableName tableName = TableName.valueOf(name.getMethodName());
325    RegionInfo[] regions = MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(),
326      tableName, splitKeys, families);
327    splitAndTruncate(tableName, regions, 1);
328  }
329
330  @Test
331  public void testTruncatePreserveWithReplicaRegionAfterSplit() throws Exception {
332    String[] families = new String[] { "f1", "f2" };
333    byte[][] splitKeys =
334      new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") };
335    TableName tableName = TableName.valueOf(name.getMethodName());
336
337    // create a table with region replications
338    TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName).setRegionReplication(3)
339      .setColumnFamilies(Arrays.stream(families)
340        .map(fam -> ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(fam)).build())
341        .collect(Collectors.toList()))
342      .build();
343    RegionInfo[] regions = ModifyRegionUtils.createRegionInfos(htd, splitKeys);
344    ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
345    long procId = ProcedureTestingUtility.submitAndWait(procExec,
346      new CreateTableProcedure(procExec.getEnvironment(), htd, regions));
347    ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId));
348
349    splitAndTruncate(tableName, regions, 3);
350  }
351
352  private void splitAndTruncate(TableName tableName, RegionInfo[] regions, int regionReplication)
353      throws IOException, InterruptedException {
354    // split a region
355    UTIL.getAdmin().split(tableName, new byte[] { '0' });
356
357    // wait until split really happens
358    UTIL.waitFor(60000,
359      () -> UTIL.getAdmin().getRegions(tableName).size() > regions.length * regionReplication);
360
361    // disable the table
362    UTIL.getAdmin().disableTable(tableName);
363
364    // truncate the table
365    ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
366    long procId = ProcedureTestingUtility.submitAndWait(procExec,
367      new TruncateTableProcedure(procExec.getEnvironment(), tableName, true));
368    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
369
370    UTIL.waitUntilAllRegionsAssigned(tableName);
371    // confirm that we have the correct number of regions
372    assertEquals((regions.length + 1) * regionReplication,
373      UTIL.getAdmin().getRegions(tableName).size());
374  }
375}