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.apache.hadoop.hbase.procedure2.ProcedureTestingUtility.assertProcNotFailed;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertFalse;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.IOException;
026import java.util.Arrays;
027import java.util.List;
028import java.util.stream.Stream;
029import org.apache.hadoop.fs.FSDataOutputStream;
030import org.apache.hadoop.fs.FileSystem;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.hbase.HBaseTestingUtil;
033import org.apache.hadoop.hbase.HConstants;
034import org.apache.hadoop.hbase.MetaTableAccessor;
035import org.apache.hadoop.hbase.TableName;
036import org.apache.hadoop.hbase.client.Admin;
037import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
038import org.apache.hadoop.hbase.client.Put;
039import org.apache.hadoop.hbase.client.RegionInfo;
040import org.apache.hadoop.hbase.client.RegionInfoBuilder;
041import org.apache.hadoop.hbase.client.Table;
042import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
043import org.apache.hadoop.hbase.client.TableState;
044import org.apache.hadoop.hbase.master.HMaster;
045import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
046import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
047import org.apache.hadoop.hbase.regionserver.HRegionServer;
048import org.apache.hadoop.hbase.testclassification.LargeTests;
049import org.apache.hadoop.hbase.testclassification.MasterTests;
050import org.apache.hadoop.hbase.util.Bytes;
051import org.apache.hadoop.hbase.util.CommonFSUtils;
052import org.junit.jupiter.api.AfterEach;
053import org.junit.jupiter.api.BeforeEach;
054import org.junit.jupiter.api.Tag;
055import org.junit.jupiter.api.Test;
056
057@Tag(MasterTests.TAG)
058@Tag(LargeTests.TAG)
059public class TestRefreshMetaProcedureIntegration {
060
061  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
062  private Admin admin;
063  private ProcedureExecutor<MasterProcedureEnv> procExecutor;
064  private HMaster master;
065  private HRegionServer regionServer;
066
067  @BeforeEach
068  public void setup() throws Exception {
069    // Start in active mode
070    TEST_UTIL.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
071
072    TEST_UTIL.startMiniCluster();
073    admin = TEST_UTIL.getAdmin();
074    procExecutor = TEST_UTIL.getHBaseCluster().getMaster().getMasterProcedureExecutor();
075    master = TEST_UTIL.getHBaseCluster().getMaster();
076    regionServer = TEST_UTIL.getHBaseCluster().getRegionServerThreads().get(0).getRegionServer();
077  }
078
079  @AfterEach
080  public void tearDown() throws Exception {
081    if (admin != null) {
082      admin.close();
083    }
084    TEST_UTIL.shutdownMiniCluster();
085  }
086
087  @Test
088  public void testRestoreMissingRegionInMeta() throws Exception {
089
090    TableName tableName = TableName.valueOf("replicaTestTable");
091
092    createTableWithData(tableName);
093
094    List<RegionInfo> activeRegions = admin.getRegions(tableName);
095    assertTrue(activeRegions.size() >= 2, "Should have at least 2 regions after split");
096
097    Table metaTable = TEST_UTIL.getConnection().getTable(TableName.META_TABLE_NAME);
098    RegionInfo regionToRemove = activeRegions.get(0);
099    admin.unassign(regionToRemove.getRegionName(), false);
100    Thread.sleep(1000);
101
102    org.apache.hadoop.hbase.client.Delete delete =
103      new org.apache.hadoop.hbase.client.Delete(regionToRemove.getRegionName());
104    metaTable.delete(delete);
105    metaTable.close();
106
107    List<RegionInfo> regionsAfterDrift = admin.getRegions(tableName);
108    assertEquals(activeRegions.size() - 1, regionsAfterDrift.size(),
109      "Should have one less region in meta after simulating drift");
110
111    setReadOnlyMode(true);
112
113    boolean writeBlocked = false;
114    try {
115      Table readOnlyTable = TEST_UTIL.getConnection().getTable(tableName);
116      Put testPut = new Put(Bytes.toBytes("test_readonly"));
117      testPut.addColumn(Bytes.toBytes("cf1"), Bytes.toBytes("qual"), Bytes.toBytes("should_fail"));
118      readOnlyTable.put(testPut);
119      readOnlyTable.close();
120    } catch (Exception e) {
121      if (e.getMessage().contains("Operation not allowed in Read-Only Mode")) {
122        writeBlocked = true;
123      }
124    }
125    assertTrue(writeBlocked, "Write operations should be blocked in read-only mode");
126
127    Long procId = admin.refreshMeta();
128
129    waitForProcedureCompletion(procId);
130
131    List<RegionInfo> regionsAfterRefresh = admin.getRegions(tableName);
132    assertEquals(activeRegions.size(), regionsAfterRefresh.size(),
133      "Missing regions should be restored by refresh_meta");
134
135    boolean regionRestored = regionsAfterRefresh.stream()
136      .anyMatch(r -> r.getRegionNameAsString().equals(regionToRemove.getRegionNameAsString()));
137    assertTrue(regionRestored, "Missing region should be restored by refresh_meta");
138
139    setReadOnlyMode(false);
140
141    Table activeTable = TEST_UTIL.getConnection().getTable(tableName);
142    Put testPut = new Put(Bytes.toBytes("test_active_again"));
143    testPut.addColumn(Bytes.toBytes("cf1"), Bytes.toBytes("qual"),
144      Bytes.toBytes("active_mode_again"));
145    activeTable.put(testPut);
146    activeTable.close();
147  }
148
149  @Test
150  public void testPhantomTableCleanup() throws Exception {
151    TableName table1 = TableName.valueOf("table1");
152    TableName phantomTable = TableName.valueOf("phantomTable");
153    createTableWithData(table1);
154    createTableWithData(phantomTable);
155
156    assertTrue(admin.getRegions(table1).size() >= 2, "Table1 should have multiple regions");
157    assertTrue(admin.getRegions(phantomTable).size() >= 2,
158      "phantomTable should have multiple regions");
159
160    deleteTableFromFilesystem(phantomTable);
161    List<TableName> tablesBeforeRefresh = Arrays.asList(admin.listTableNames());
162    assertTrue(tablesBeforeRefresh.contains(phantomTable),
163      "phantomTable should still be listed before refresh_meta");
164    assertTrue(tablesBeforeRefresh.contains(table1), "Table1 should still be listed");
165
166    setReadOnlyMode(true);
167    Long procId = admin.refreshMeta();
168    waitForProcedureCompletion(procId);
169
170    List<TableName> tablesAfterRefresh = Arrays.asList(admin.listTableNames());
171
172    assertFalse(tablesAfterRefresh.contains(phantomTable),
173      "phantomTable should be removed after refresh_meta");
174    assertTrue(tablesAfterRefresh.contains(table1), "Table1 should still be listed");
175    assertTrue(admin.getRegions(phantomTable).isEmpty(),
176      "phantomTable should have no regions after refresh_meta");
177    setReadOnlyMode(false);
178  }
179
180  @Test
181  public void testRestoreTableStateForOrphanRegions() throws Exception {
182    TableName tableName = TableName.valueOf("t1");
183    createTableInFilesystem(tableName);
184
185    assertEquals(0, Stream.of(admin.listTableNames()).filter(tn -> tn.equals(tableName)).count(),
186      "No tables should exist");
187
188    setReadOnlyMode(true);
189    Long procId = admin.refreshMeta();
190    waitForProcedureCompletion(procId);
191
192    TableState tableState = MetaTableAccessor.getTableState(admin.getConnection(), tableName);
193    assert tableState != null;
194    assertEquals(TableState.State.ENABLED, tableState.getState(), "Table state should be ENABLED");
195    assertEquals(1, Stream.of(admin.listTableNames()).filter(tn -> tn.equals(tableName)).count(),
196      "The list should show the new table from the FS");
197    assertFalse(admin.getRegions(tableName).isEmpty(), "Should have at least 1 region");
198    setReadOnlyMode(false);
199  }
200
201  private void createTableInFilesystem(TableName tableName) throws IOException {
202    FileSystem fs = TEST_UTIL.getTestFileSystem();
203    Path rootDir = CommonFSUtils.getRootDir(TEST_UTIL.getConfiguration());
204    Path tableDir = CommonFSUtils.getTableDir(rootDir, tableName);
205    fs.mkdirs(tableDir);
206
207    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName);
208    TEST_UTIL.getHBaseCluster().getMaster().getTableDescriptors()
209      .update(builder.setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf1")).build(), false);
210
211    Path regionDir = new Path(tableDir, "dab6d1e1c88787c13b97647f11b2c907");
212    Path regionInfoFile = new Path(regionDir, HRegionFileSystem.REGION_INFO_FILE);
213    fs.mkdirs(regionDir);
214
215    RegionInfo regionInfo = RegionInfoBuilder.newBuilder(tableName).setStartKey(new byte[0])
216      .setEndKey(new byte[0]).setRegionId(1757100253228L).build();
217    byte[] regionInfoContent = RegionInfo.toDelimitedByteArray(regionInfo);
218    try (FSDataOutputStream out = fs.create(regionInfoFile, true)) {
219      out.write(regionInfoContent);
220    }
221  }
222
223  private void deleteTableFromFilesystem(TableName tableName) throws IOException {
224    FileSystem fs = TEST_UTIL.getTestFileSystem();
225    Path rootDir = CommonFSUtils.getRootDir(TEST_UTIL.getConfiguration());
226    Path tableDir = CommonFSUtils.getTableDir(rootDir, tableName);
227    if (fs.exists(tableDir)) {
228      fs.delete(tableDir, true);
229    }
230  }
231
232  private void createTableWithData(TableName tableName) throws Exception {
233    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName);
234    builder.setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf1"));
235    byte[] splitKeyBytes = Bytes.toBytes("split_key");
236    admin.createTable(builder.build(), new byte[][] { splitKeyBytes });
237    TEST_UTIL.waitTableAvailable(tableName);
238    try (Table table = TEST_UTIL.getConnection().getTable(tableName)) {
239      for (int i = 0; i < 100; i++) {
240        Put put = new Put(Bytes.toBytes("row_" + String.format("%05d", i)));
241        put.addColumn(Bytes.toBytes("cf1"), Bytes.toBytes("qual"), Bytes.toBytes("value_" + i));
242        table.put(put);
243      }
244    }
245    admin.flush(tableName);
246  }
247
248  private void waitForProcedureCompletion(Long procId) {
249    assertTrue(procId > 0, "Procedure ID should be positive");
250    TEST_UTIL.waitFor(1000, () -> {
251      try {
252        return procExecutor.isFinished(procId);
253      } catch (Exception e) {
254        return false;
255      }
256    });
257    assertProcNotFailed(procExecutor.getResult(procId));
258  }
259
260  private void setReadOnlyMode(boolean isReadOnly) {
261    TEST_UTIL.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
262      isReadOnly);
263    notifyConfigurationObservers();
264  }
265
266  private void notifyConfigurationObservers() {
267    master.getConfigurationManager().notifyAllObservers(TEST_UTIL.getConfiguration());
268    regionServer.getConfigurationManager().notifyAllObservers(TEST_UTIL.getConfiguration());
269  }
270}