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().getTableRegions(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.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().getTableRegions(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(
226      UTIL.getHBaseCluster().getMaster(), 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(
233      UTIL.getConnection(), tableName, 50, splitKeys, 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)
260      throws HBaseIOException {
261      super(env, tableName, preserveSplits);
262    }
263
264    @Override
265    protected Flow executeFromState(MasterProcedureEnv env,
266      MasterProcedureProtos.TruncateTableState state) throws InterruptedException {
267
268      if (!failOnce &&
269        state == MasterProcedureProtos.TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT) {
270        try {
271          // To emulate an HDFS failure, create only the first region directory
272          RegionInfo regionInfo = getFirstRegionInfo();
273          Configuration conf = env.getMasterConfiguration();
274          MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
275          Path tempdir = mfs.getTempDir();
276          Path tableDir = FSUtils.getTableDir(tempdir, regionInfo.getTable());
277          Path regionDir = FSUtils.getRegionDirFromTableDir(tableDir, regionInfo);
278          FileSystem fs = FileSystem.get(conf);
279          fs.mkdirs(regionDir);
280
281          failOnce = true;
282          return Flow.HAS_MORE_STATE;
283        } catch (IOException e) {
284          fail("failed to create a region directory: " + e);
285        }
286      }
287
288      return super.executeFromState(env, state);
289    }
290  }
291
292  private void testOnHDFSFailure(TableName tableName, boolean preserveSplits) throws Exception {
293    String[] families = new String[] { "f1", "f2" };
294    byte[][] splitKeys = new byte[][] {
295      Bytes.toBytes("a"), Bytes.toBytes("b"), Bytes.toBytes("c")
296    };
297
298    // create a table
299    MasterProcedureTestingUtility.createTable(
300      getMasterProcedureExecutor(), tableName, splitKeys, families);
301
302    // load and verify that there are rows in the table
303    MasterProcedureTestingUtility.loadData(
304      UTIL.getConnection(), tableName, 100, splitKeys, 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 = ProcedureTestingUtility.submitAndWait(procExec,
313      new TruncateTableProcedureOnHDFSFailure(procExec.getEnvironment(), tableName,
314        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}