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.assignment;
019
020import static org.apache.hadoop.hbase.master.assignment.AssignmentTestingUtil.insertData;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertThrows;
024import static org.junit.jupiter.api.Assertions.assertTrue;
025
026import java.io.IOException;
027import java.util.Collections;
028import java.util.List;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.TableName;
031import org.apache.hadoop.hbase.client.Hbck;
032import org.apache.hadoop.hbase.client.RegionInfo;
033import org.apache.hadoop.hbase.client.RegionInfoBuilder;
034import org.apache.hadoop.hbase.client.TableDescriptor;
035import org.apache.hadoop.hbase.master.RegionState;
036import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
037import org.apache.hadoop.hbase.master.procedure.MasterProcedureTestingUtility;
038import org.apache.hadoop.hbase.procedure2.Procedure;
039import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
040import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
041import org.apache.hadoop.hbase.testclassification.MasterTests;
042import org.apache.hadoop.hbase.testclassification.MediumTests;
043import org.apache.hadoop.hbase.util.Bytes;
044import org.junit.jupiter.api.AfterAll;
045import org.junit.jupiter.api.AfterEach;
046import org.junit.jupiter.api.BeforeAll;
047import org.junit.jupiter.api.BeforeEach;
048import org.junit.jupiter.api.Tag;
049import org.junit.jupiter.api.Test;
050import org.junit.jupiter.api.TestInfo;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054/**
055 * Regression test for HBASE-30353: a split parent region must never be re-opened after master
056 * failover.
057 * <p>
058 * The fix has two parts. {@code RegionStateStore.splitRegion} now writes {@code SPLIT} into
059 * {@code info:state} for the parent; that is covered by {@code TestRegionStateStore}. This class
060 * covers the second part, the {@code isSplit()} check in
061 * {@code AssignmentManager.createAssignProcedure}. It matters for parents loaded from meta rows
062 * written before the fix (state {@code CLOSED} with {@code regionInfo.isSplit()=true}), which pass
063 * the expected-state check, and for forced assigns, which skip it. The tests go through the client
064 * entry points, {@code Admin.assign} and {@code Hbck.assigns}.
065 */
066@Tag(MasterTests.TAG)
067@Tag(MediumTests.TAG)
068public class TestSplitParentAssignment {
069
070  private static final Logger LOG = LoggerFactory.getLogger(TestSplitParentAssignment.class);
071
072  private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
073  private static final String CF = "cf";
074  private static final int ROW_COUNT = 60;
075  private static final int START_ROW = 11;
076
077  private String testMethodName;
078
079  @BeforeAll
080  public static void setupCluster() throws Exception {
081    UTIL.startMiniCluster(3);
082  }
083
084  @AfterAll
085  public static void tearDownCluster() throws IOException {
086    try {
087      UTIL.shutdownMiniCluster();
088    } catch (IOException e) {
089      LOG.warn("failure shutting down cluster", e);
090    }
091  }
092
093  @BeforeEach
094  public void setup(TestInfo testInfo) throws Exception {
095    testMethodName = testInfo.getTestMethod().get().getName();
096    // Prevent CatalogJanitor from garbage collecting the split parent before the test can use it.
097    UTIL.getHBaseCluster().getMaster().setCatalogJanitorEnabled(false);
098    // Prevent compaction. If daughters compact away reference files, the parent becomes
099    // eligible for garbage collection even with CatalogJanitor disabled.
100    for (int i = 0; i < UTIL.getHBaseCluster().getLiveRegionServerThreads().size(); i++) {
101      UTIL.getHBaseCluster().getRegionServer(i).getCompactSplitThread().switchCompaction(false);
102    }
103  }
104
105  @AfterEach
106  public void tearDown() throws Exception {
107    UTIL.getHBaseCluster().getMaster().setCatalogJanitorEnabled(true);
108    for (TableDescriptor htd : UTIL.getAdmin().listTableDescriptors()) {
109      UTIL.deleteTable(htd.getTableName());
110    }
111  }
112
113  @Test
114  public void testAdminAssignRejectsSplitParentLoadedAsClosed() throws Exception {
115    TableName tableName = TableName.valueOf(testMethodName);
116    AssignmentManager am = UTIL.getHBaseCluster().getMaster().getAssignmentManager();
117    RegionInfo parentInfo = splitTableAndGetParent(tableName);
118    RegionStateNode rsn = simulatePostFailover(am, parentInfo, RegionState.State.CLOSED);
119    RegionInfo splitParentInfo = rsn.getRegionInfo();
120
121    Exception ex =
122      assertThrows(Exception.class, () -> UTIL.getAdmin().assign(splitParentInfo.getRegionName()),
123        "Admin.assign must reject a split parent even when its state is CLOSED");
124
125    assertTrue(ex.toString().contains("is a split parent and cannot be assigned"),
126      "Expected the split parent guard to reject the assign but got: " + ex);
127    assertNull(rsn.getProcedure(), "No assign procedure may be attached to a split parent");
128    assertEquals(RegionState.State.CLOSED, rsn.getState());
129  }
130
131  @Test
132  public void testHbckForceAssignRejectsSplitParentLoadedAsClosed() throws Exception {
133    TableName tableName = TableName.valueOf(testMethodName);
134    AssignmentManager am = UTIL.getHBaseCluster().getMaster().getAssignmentManager();
135    RegionInfo parentInfo = splitTableAndGetParent(tableName);
136    RegionStateNode rsn = simulatePostFailover(am, parentInfo, RegionState.State.CLOSED);
137    RegionInfo splitParentInfo = rsn.getRegionInfo();
138
139    List<Long> pids;
140    try (Hbck hbck = UTIL.getConnection().getHbck()) {
141      pids = hbck.assigns(Collections.singletonList(splitParentInfo.getEncodedName()), true, true);
142    }
143
144    assertEquals(Collections.singletonList(Procedure.NO_PROC_ID), pids,
145      "Forced HBCK assign of a split parent must not submit a procedure");
146    assertNull(rsn.getProcedure(), "No assign procedure may be attached to a split parent");
147    assertEquals(RegionState.State.CLOSED, rsn.getState());
148  }
149
150  private RegionInfo splitTableAndGetParent(TableName tableName) throws Exception {
151    ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
152    RegionInfo[] regions = MasterProcedureTestingUtility.createTable(procExec, tableName, null, CF);
153    insertData(UTIL, tableName, ROW_COUNT, START_ROW, CF);
154    byte[] splitKey = Bytes.toBytes("" + (START_ROW + ROW_COUNT / 2));
155    long procId = procExec.submitProcedure(
156      new SplitTableRegionProcedure(procExec.getEnvironment(), regions[0], splitKey));
157    ProcedureTestingUtility.waitProcedure(procExec, procId);
158    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
159    return regions[0];
160  }
161
162  /**
163   * Replaces the parent's {@link RegionStateNode} with one shaped like {@code loadMeta} would
164   * rebuild it after a master failover: {@code regionInfo.isSplit()=true} and the given state. Pass
165   * {@code CLOSED} to reproduce a meta row written before HBASE-30353, where the parent's
166   * {@code info:state} was not {@code SPLIT}.
167   */
168  private RegionStateNode simulatePostFailover(AssignmentManager am, RegionInfo parentInfo,
169    RegionState.State state) {
170    am.getRegionStates().deleteRegion(parentInfo);
171    RegionInfo splitParentInfo = RegionInfoBuilder.newBuilder(parentInfo.getTable())
172      .setStartKey(parentInfo.getStartKey()).setEndKey(parentInfo.getEndKey())
173      .setRegionId(parentInfo.getRegionId()).setSplit(true).setOffline(true).build();
174    RegionStateNode freshRsn = am.getRegionStates().getOrCreateRegionStateNode(splitParentInfo);
175    freshRsn.setState(state);
176    return freshRsn;
177  }
178
179  private ProcedureExecutor<MasterProcedureEnv> getMasterProcedureExecutor() {
180    return UTIL.getHBaseCluster().getMaster().getMasterProcedureExecutor();
181  }
182}