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 = new byte[][] {
135      Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c")
136    };
137
138    RegionInfo[] regions = MasterProcedureTestingUtility.createTable(
139      getMasterProcedureExecutor(), tableName, splitKeys, families);
140    // load and verify that there are rows in the table
141    MasterProcedureTestingUtility.loadData(
142      UTIL.getConnection(), tableName, 100, splitKeys, families);
143    assertEquals(100, UTIL.countRows(tableName));
144    // disable the table
145    UTIL.getAdmin().disableTable(tableName);
146
147    // truncate the table
148    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
149    long procId = ProcedureTestingUtility.submitAndWait(procExec,
150      new TruncateTableProcedure(procExec.getEnvironment(), tableName, preserveSplits));
151    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
152
153    // If truncate procedure completed successfully, it means all regions were assigned correctly
154    // and table is enabled now.
155    UTIL.waitUntilAllRegionsAssigned(tableName);
156
157    // validate the table regions and layout
158    regions = UTIL.getAdmin().getRegions(tableName).toArray(new RegionInfo[0]);
159    if (preserveSplits) {
160      assertEquals(1 + splitKeys.length, regions.length);
161    } else {
162      assertEquals(1, regions.length);
163    }
164    MasterProcedureTestingUtility.validateTableCreation(
165      UTIL.getHBaseCluster().getMaster(), tableName, regions, families);
166
167    // verify that there are no rows in the table
168    assertEquals(0, UTIL.countRows(tableName));
169
170    // verify that the table is read/writable
171    MasterProcedureTestingUtility.loadData(
172      UTIL.getConnection(), tableName, 50, splitKeys, families);
173    assertEquals(50, UTIL.countRows(tableName));
174  }
175
176  @Test
177  public void testRecoveryAndDoubleExecutionPreserveSplits() throws Exception {
178    final TableName tableName = TableName.valueOf(name.getMethodName());
179    testRecoveryAndDoubleExecution(tableName, true);
180  }
181
182  @Test
183  public void testRecoveryAndDoubleExecutionNoPreserveSplits() throws Exception {
184    final TableName tableName = TableName.valueOf(name.getMethodName());
185    testRecoveryAndDoubleExecution(tableName, false);
186  }
187
188  private void testRecoveryAndDoubleExecution(final TableName tableName,
189      final boolean preserveSplits) throws Exception {
190    final String[] families = new String[] { "f1", "f2" };
191
192    // create the table
193    final byte[][] splitKeys = new byte[][] {
194      Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c")
195    };
196    RegionInfo[] regions = MasterProcedureTestingUtility.createTable(
197      getMasterProcedureExecutor(), tableName, splitKeys, families);
198    // load and verify that there are rows in the table
199    MasterProcedureTestingUtility.loadData(
200      UTIL.getConnection(), tableName, 100, splitKeys, families);
201    assertEquals(100, UTIL.countRows(tableName));
202    // disable the table
203    UTIL.getAdmin().disableTable(tableName);
204
205    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
206    ProcedureTestingUtility.waitNoProcedureRunning(procExec);
207    ProcedureTestingUtility.setKillIfHasParent(procExec, false);
208    ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true);
209
210    // Start the Truncate procedure && kill the executor
211    long procId = procExec.submitProcedure(
212      new TruncateTableProcedure(procExec.getEnvironment(), tableName, preserveSplits));
213
214    // Restart the executor and execute the step twice
215    MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId);
216
217    ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, false);
218    UTIL.waitUntilAllRegionsAssigned(tableName);
219
220    // validate the table regions and layout
221    regions = UTIL.getAdmin().getRegions(tableName).toArray(new RegionInfo[0]);
222    if (preserveSplits) {
223      assertEquals(1 + splitKeys.length, regions.length);
224    } else {
225      assertEquals(1, regions.length);
226    }
227    MasterProcedureTestingUtility.validateTableCreation(
228      UTIL.getHBaseCluster().getMaster(), tableName, regions, families);
229
230    // verify that there are no rows in the table
231    assertEquals(0, UTIL.countRows(tableName));
232
233    // verify that the table is read/writable
234    MasterProcedureTestingUtility.loadData(
235      UTIL.getConnection(), tableName, 50, splitKeys, families);
236    assertEquals(50, UTIL.countRows(tableName));
237  }
238
239  @Test
240  public void testOnHDFSFailurePreserveSplits() throws Exception {
241    final TableName tableName = TableName.valueOf(name.getMethodName());
242    testOnHDFSFailure(tableName, true);
243  }
244
245  @Test
246  public void testOnHDFSFailureNoPreserveSplits() throws Exception {
247    final TableName tableName = TableName.valueOf(name.getMethodName());
248    testOnHDFSFailure(tableName, false);
249  }
250
251  public static class TruncateTableProcedureOnHDFSFailure extends TruncateTableProcedure {
252
253    private boolean failOnce = false;
254
255    public TruncateTableProcedureOnHDFSFailure() {
256      // Required by the Procedure framework to create the procedure on replay
257      super();
258    }
259
260    public TruncateTableProcedureOnHDFSFailure(final MasterProcedureEnv env, TableName tableName,
261      boolean preserveSplits)
262      throws HBaseIOException {
263      super(env, tableName, preserveSplits);
264    }
265
266    @Override
267    protected Flow executeFromState(MasterProcedureEnv env,
268      MasterProcedureProtos.TruncateTableState state) throws InterruptedException {
269
270      if (!failOnce &&
271        state == MasterProcedureProtos.TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT) {
272        try {
273          // To emulate an HDFS failure, create only the first region directory
274          RegionInfo regionInfo = getFirstRegionInfo();
275          Configuration conf = env.getMasterConfiguration();
276          MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
277          Path tempdir = mfs.getTempDir();
278          Path tableDir = CommonFSUtils.getTableDir(tempdir, regionInfo.getTable());
279          Path regionDir = FSUtils.getRegionDirFromTableDir(tableDir, regionInfo);
280          FileSystem fs = FileSystem.get(conf);
281          fs.mkdirs(regionDir);
282
283          failOnce = true;
284          return Flow.HAS_MORE_STATE;
285        } catch (IOException e) {
286          fail("failed to create a region directory: " + e);
287        }
288      }
289
290      return super.executeFromState(env, state);
291    }
292  }
293
294  private void testOnHDFSFailure(TableName tableName, boolean preserveSplits) throws Exception {
295    String[] families = new String[] { "f1", "f2" };
296    byte[][] splitKeys = new byte[][] {
297      Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c")
298    };
299
300    // create a table
301    MasterProcedureTestingUtility.createTable(
302      getMasterProcedureExecutor(), tableName, splitKeys, families);
303
304    // load and verify that there are rows in the table
305    MasterProcedureTestingUtility.loadData(
306      UTIL.getConnection(), tableName, 100, splitKeys, families);
307    assertEquals(100, UTIL.countRows(tableName));
308
309    // disable the table
310    UTIL.getAdmin().disableTable(tableName);
311
312    // truncate the table
313    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
314    long procId = ProcedureTestingUtility.submitAndWait(procExec,
315      new TruncateTableProcedureOnHDFSFailure(procExec.getEnvironment(), tableName,
316        preserveSplits));
317    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
318  }
319
320  @Test
321  public void testTruncateWithPreserveAfterSplit() throws Exception {
322    String[] families = new String[] { "f1", "f2" };
323    byte[][] splitKeys =
324      new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") };
325    TableName tableName = TableName.valueOf(name.getMethodName());
326    RegionInfo[] regions = MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(),
327      tableName, splitKeys, families);
328    splitAndTruncate(tableName, regions, 1);
329  }
330
331  @Test
332  public void testTruncatePreserveWithReplicaRegionAfterSplit() throws Exception {
333    String[] families = new String[] { "f1", "f2" };
334    byte[][] splitKeys =
335      new byte[][] { Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c") };
336    TableName tableName = TableName.valueOf(name.getMethodName());
337
338    // create a table with region replications
339    TableDescriptor htd = TableDescriptorBuilder.newBuilder(tableName).setRegionReplication(3)
340      .setColumnFamilies(Arrays.stream(families)
341        .map(fam -> ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(fam)).build())
342        .collect(Collectors.toList()))
343      .build();
344    RegionInfo[] regions = ModifyRegionUtils.createRegionInfos(htd, splitKeys);
345    ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
346    long procId = ProcedureTestingUtility.submitAndWait(procExec,
347      new CreateTableProcedure(procExec.getEnvironment(), htd, regions));
348    ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId));
349
350    splitAndTruncate(tableName, regions, 3);
351  }
352
353  private void splitAndTruncate(TableName tableName, RegionInfo[] regions, int regionReplication)
354      throws IOException, InterruptedException {
355    // split a region
356    UTIL.getAdmin().split(tableName, new byte[] { '0' });
357
358    // wait until split really happens
359    UTIL.waitFor(60000,
360      () -> UTIL.getAdmin().getRegions(tableName).size() > regions.length * regionReplication);
361
362    // disable the table
363    UTIL.getAdmin().disableTable(tableName);
364
365    // truncate the table
366    ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
367    long procId = ProcedureTestingUtility.submitAndWait(procExec,
368      new TruncateTableProcedure(procExec.getEnvironment(), tableName, true));
369    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
370
371    UTIL.waitUntilAllRegionsAssigned(tableName);
372    // confirm that we have the correct number of regions
373    assertEquals((regions.length + 1) * regionReplication,
374      UTIL.getAdmin().getRegions(tableName).size());
375  }
376}