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.client.TableDescriptor;
029import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
030import org.apache.hadoop.hbase.procedure2.Procedure;
031import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
032import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
033import org.apache.hadoop.hbase.testclassification.LargeTests;
034import org.apache.hadoop.hbase.testclassification.MasterTests;
035import org.junit.BeforeClass;
036import org.junit.ClassRule;
037import org.junit.Rule;
038import org.junit.Test;
039import org.junit.experimental.categories.Category;
040import org.junit.rules.TestName;
041
042import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.ModifyTableState;
043
044@Category({ MasterTests.class, LargeTests.class })
045public class TestModifyTableProcedureWithRecovery extends TestTableDDLProcedureBase {
046
047  @ClassRule
048  public static final HBaseClassTestRule CLASS_RULE =
049    HBaseClassTestRule.forClass(TestModifyTableProcedureWithRecovery.class);
050
051  @Rule
052  public TestName name = new TestName();
053
054  @BeforeClass
055  public static void setupCluster() throws Exception {
056    // Enable recovery snapshots
057    UTIL.getConfiguration().setBoolean(HConstants.SNAPSHOT_BEFORE_DESTRUCTIVE_ACTION_ENABLED_KEY,
058      true);
059    TestTableDDLProcedureBase.setupCluster();
060  }
061
062  @Test
063  public void testRecoverySnapshotRollback() throws Exception {
064    final TableName tableName = TableName.valueOf(name.getMethodName());
065    final String cf1 = "cf1";
066    final String cf2 = "cf2";
067    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
068
069    // Create table with multiple column families
070    MasterProcedureTestingUtility.createTable(procExec, tableName, null, cf1, cf2);
071    MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, new byte[0][],
072      new String[] { cf1, cf2 });
073    UTIL.getAdmin().disableTable(tableName);
074
075    // Create a procedure that will fail - modify to delete a column family
076    // but simulate failure after snapshot creation
077    // Modify table to remove cf2 (which should trigger recovery snapshot)
078    TableDescriptor originalHtd = UTIL.getAdmin().getDescriptor(tableName);
079    TableDescriptor modifiedHtd =
080      TableDescriptorBuilder.newBuilder(originalHtd).removeColumnFamily(cf2.getBytes()).build();
081
082    // Submit the failing procedure
083    long procId = procExec
084      .submitProcedure(new FailingModifyTableProcedure(procExec.getEnvironment(), modifiedHtd));
085
086    // Wait for procedure to complete (should fail)
087    ProcedureTestingUtility.waitProcedure(procExec, procId);
088    Procedure<MasterProcedureEnv> result = procExec.getResult(procId);
089    assertTrue("Procedure should have failed", result.isFailed());
090
091    // Verify no recovery snapshots remain after rollback
092    boolean snapshotFound = false;
093    for (SnapshotDescription snapshot : UTIL.getAdmin().listSnapshots()) {
094      if (snapshot.getName().startsWith("auto_" + tableName.getNameAsString())) {
095        snapshotFound = true;
096        break;
097      }
098    }
099    assertTrue("Recovery snapshot should have been cleaned up during rollback", !snapshotFound);
100  }
101
102  @Test
103  public void testRecoverySnapshotAndRestore() throws Exception {
104    final TableName tableName = TableName.valueOf(name.getMethodName());
105    final TableName restoredTableName = TableName.valueOf(name.getMethodName() + "_restored");
106    final String cf1 = "cf1";
107    final String cf2 = "cf2";
108    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
109
110    // Create table with multiple column families
111    MasterProcedureTestingUtility.createTable(procExec, tableName, null, cf1, cf2);
112    MasterProcedureTestingUtility.loadData(UTIL.getConnection(), tableName, 100, new byte[0][],
113      new String[] { cf1, cf2 });
114    UTIL.getAdmin().disableTable(tableName);
115
116    // Modify table to remove cf2 (which should trigger recovery snapshot)
117    TableDescriptor originalHtd = UTIL.getAdmin().getDescriptor(tableName);
118    TableDescriptor modifiedHtd =
119      TableDescriptorBuilder.newBuilder(originalHtd).removeColumnFamily(cf2.getBytes()).build();
120
121    long procId = ProcedureTestingUtility.submitAndWait(procExec,
122      new ModifyTableProcedure(procExec.getEnvironment(), modifiedHtd));
123    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
124
125    // Verify table modification was successful
126    TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName);
127    assertEquals("Should have one column family", 1, currentHtd.getColumnFamilyNames().size());
128    assertTrue("Should only have cf1", currentHtd.hasColumnFamily(cf1.getBytes()));
129
130    // Find the recovery snapshot
131    String recoverySnapshotName = null;
132    for (SnapshotDescription snapshot : UTIL.getAdmin().listSnapshots()) {
133      if (snapshot.getName().startsWith("auto_" + tableName.getNameAsString())) {
134        recoverySnapshotName = snapshot.getName();
135        break;
136      }
137    }
138    assertTrue("Recovery snapshot should exist", recoverySnapshotName != null);
139
140    // Restore from snapshot by cloning to a new table
141    UTIL.getAdmin().cloneSnapshot(recoverySnapshotName, restoredTableName);
142    UTIL.waitUntilAllRegionsAssigned(restoredTableName);
143
144    // Verify restored table has original structure with both column families
145    TableDescriptor restoredHtd = UTIL.getAdmin().getDescriptor(restoredTableName);
146    assertEquals("Should have two column families", 2, restoredHtd.getColumnFamilyNames().size());
147    assertTrue("Should have cf1", restoredHtd.hasColumnFamily(cf1.getBytes()));
148    assertTrue("Should have cf2", restoredHtd.hasColumnFamily(cf2.getBytes()));
149
150    // Clean up the cloned table
151    UTIL.getAdmin().disableTable(restoredTableName);
152    UTIL.getAdmin().deleteTable(restoredTableName);
153  }
154
155  public static class FailingModifyTableProcedure extends ModifyTableProcedure {
156    private boolean failOnce = false;
157
158    public FailingModifyTableProcedure() {
159      super();
160    }
161
162    public FailingModifyTableProcedure(MasterProcedureEnv env, TableDescriptor newTableDescriptor)
163      throws HBaseIOException {
164      super(env, newTableDescriptor);
165    }
166
167    @Override
168    protected Flow executeFromState(MasterProcedureEnv env, ModifyTableState state)
169      throws InterruptedException {
170      if (!failOnce && state == ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS) {
171        failOnce = true;
172        throw new RuntimeException("Simulated failure");
173      }
174      return super.executeFromState(env, state);
175    }
176  }
177}