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;
022
023import org.apache.hadoop.hbase.HBaseClassTestRule;
024import org.apache.hadoop.hbase.HBaseIOException;
025import org.apache.hadoop.hbase.HConstants;
026import org.apache.hadoop.hbase.TableName;
027import org.apache.hadoop.hbase.client.SnapshotDescription;
028import org.apache.hadoop.hbase.procedure2.Procedure;
029import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
030import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
031import org.apache.hadoop.hbase.testclassification.LargeTests;
032import org.apache.hadoop.hbase.testclassification.MasterTests;
033import org.junit.BeforeClass;
034import org.junit.ClassRule;
035import org.junit.Rule;
036import org.junit.Test;
037import org.junit.experimental.categories.Category;
038import org.junit.rules.TestName;
039
040import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.TruncateTableState;
041
042@Category({ MasterTests.class, LargeTests.class })
043public class TestTruncateTableProcedureWithRecovery extends TestTableDDLProcedureBase {
044
045  @ClassRule
046  public static final HBaseClassTestRule CLASS_RULE =
047    HBaseClassTestRule.forClass(TestTruncateTableProcedureWithRecovery.class);
048
049  @Rule
050  public TestName name = new TestName();
051
052  @BeforeClass
053  public static void setupCluster() throws Exception {
054    // Enable recovery snapshots
055    UTIL.getConfiguration().setBoolean(HConstants.SNAPSHOT_BEFORE_DESTRUCTIVE_ACTION_ENABLED_KEY,
056      true);
057    TestTableDDLProcedureBase.setupCluster();
058  }
059
060  @Test
061  public void testRecoverySnapshotRollback() throws Exception {
062    final TableName tableName = TableName.valueOf(name.getMethodName());
063    final String[] families = new String[] { "f1", "f2" };
064    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
065
066    // Create table with data
067    MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), tableName, null,
068      families);
069    MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, new byte[0][],
070      families);
071    assertEquals(100, UTIL.countRows(tableName));
072
073    // Disable the table
074    UTIL.getAdmin().disableTable(tableName);
075
076    // Submit the failing procedure
077    long procId = procExec.submitProcedure(
078      new FailingTruncateTableProcedure(procExec.getEnvironment(), tableName, false));
079
080    // Wait for procedure to complete (should fail)
081    ProcedureTestingUtility.waitProcedure(procExec, procId);
082    Procedure<MasterProcedureEnv> result = procExec.getResult(procId);
083    assertTrue("Procedure should have failed", result.isFailed());
084
085    // Verify no recovery snapshots remain after rollback
086    boolean snapshotFound = false;
087    for (SnapshotDescription snapshot : UTIL.getAdmin().listSnapshots()) {
088      if (snapshot.getName().startsWith("auto_" + tableName.getNameAsString())) {
089        snapshotFound = true;
090        break;
091      }
092    }
093    assertTrue("Recovery snapshot should have been cleaned up during rollback", !snapshotFound);
094  }
095
096  @Test
097  public void testRecoverySnapshotAndRestore() throws Exception {
098    final TableName tableName = TableName.valueOf(name.getMethodName());
099    final TableName restoredTableName = TableName.valueOf(name.getMethodName() + "_restored");
100    final String[] families = new String[] { "f1", "f2" };
101
102    // Create table with data
103    MasterProcedureTestingUtility.createTable(getMasterProcedureExecutor(), tableName, null,
104      families);
105    MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, new byte[0][],
106      families);
107    assertEquals(100, UTIL.countRows(tableName));
108
109    // Disable the table
110    UTIL.getAdmin().disableTable(tableName);
111
112    // Truncate the table (this should create a recovery snapshot)
113    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
114    long procId = ProcedureTestingUtility.submitAndWait(procExec,
115      new TruncateTableProcedure(procExec.getEnvironment(), tableName, false));
116    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
117
118    // Verify table is truncated
119    UTIL.waitUntilAllRegionsAssigned(tableName);
120    assertEquals(0, UTIL.countRows(tableName));
121
122    // Find the recovery snapshot
123    String recoverySnapshotName = null;
124    for (SnapshotDescription snapshot : UTIL.getAdmin().listSnapshots()) {
125      if (snapshot.getName().startsWith("auto_" + tableName.getNameAsString())) {
126        recoverySnapshotName = snapshot.getName();
127        break;
128      }
129    }
130    assertTrue("Recovery snapshot should exist", recoverySnapshotName != null);
131
132    // Restore from snapshot by cloning to a new table
133    UTIL.getAdmin().cloneSnapshot(recoverySnapshotName, restoredTableName);
134    UTIL.waitUntilAllRegionsAssigned(restoredTableName);
135
136    // Verify restored table has original data
137    assertEquals(100, UTIL.countRows(restoredTableName));
138
139    // Clean up the cloned table
140    UTIL.getAdmin().disableTable(restoredTableName);
141    UTIL.getAdmin().deleteTable(restoredTableName);
142  }
143
144  public static class FailingTruncateTableProcedure extends TruncateTableProcedure {
145    private boolean failOnce = false;
146
147    public FailingTruncateTableProcedure() {
148      super();
149    }
150
151    public FailingTruncateTableProcedure(MasterProcedureEnv env, TableName tableName,
152      boolean preserveSplits) throws HBaseIOException {
153      super(env, tableName, preserveSplits);
154    }
155
156    @Override
157    protected Flow executeFromState(MasterProcedureEnv env, TruncateTableState state)
158      throws InterruptedException {
159      if (!failOnce && state == TruncateTableState.TRUNCATE_TABLE_CLEAR_FS_LAYOUT) {
160        failOnce = true;
161        throw new RuntimeException("Simulated failure");
162      }
163      return super.executeFromState(env, state);
164    }
165  }
166}