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}