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