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;
019
020import static org.junit.jupiter.api.Assertions.assertTrue;
021
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.List;
026import java.util.Map;
027import java.util.concurrent.CyclicBarrier;
028import java.util.concurrent.atomic.AtomicReference;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.ServerName;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.hadoop.hbase.client.RegionInfo;
033import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
034import org.apache.hadoop.hbase.rsgroup.RSGroupBasedLoadBalancer;
035import org.apache.hadoop.hbase.testclassification.MasterTests;
036import org.apache.hadoop.hbase.testclassification.MediumTests;
037import org.apache.hadoop.hbase.util.Bytes;
038import org.junit.jupiter.api.AfterEach;
039import org.junit.jupiter.api.BeforeEach;
040import org.junit.jupiter.api.Tag;
041import org.junit.jupiter.api.Test;
042import org.junit.jupiter.api.TestInfo;
043import org.mockito.Mockito;
044import org.mockito.invocation.InvocationOnMock;
045
046@Tag(MasterTests.TAG)
047@Tag(MediumTests.TAG)
048public class TestMasterBalancerNPE {
049
050  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
051  private static final byte[] FAMILYNAME = Bytes.toBytes("fam");
052  private String testMethodName;
053
054  @BeforeEach
055  public void setTestMethod(TestInfo testInfo) {
056    testMethodName = testInfo.getTestMethod().get().getName();
057  }
058
059  @BeforeEach
060  public void setupConfiguration() {
061    /**
062     * Make {@link BalancerChore} not run,so does not disrupt the test.
063     */
064    HMaster.setDisableBalancerChoreForTest(true);
065  }
066
067  @AfterEach
068  public void shutdown() throws Exception {
069    HMaster.setDisableBalancerChoreForTest(false);
070    TEST_UTIL.shutdownMiniCluster();
071  }
072
073  /**
074   * This test is for HBASE-26712, to make the region is unassigned just before
075   * {@link AssignmentManager#balance} is invoked on the region.
076   */
077  @Test
078  public void testBalancerNPE() throws Exception {
079    TEST_UTIL.startMiniCluster(2);
080    TEST_UTIL.getAdmin().balancerSwitch(false, true);
081    TableName tableName = createTable(testMethodName);
082    final HMaster master = TEST_UTIL.getHBaseCluster().getMaster();
083    List<RegionInfo> regionInfos = TEST_UTIL.getAdmin().getRegions(tableName);
084    assertTrue(regionInfos.size() == 1);
085    final ServerName serverName1 =
086      TEST_UTIL.getMiniHBaseCluster().getRegionServer(0).getServerName();
087    final ServerName serverName2 =
088      TEST_UTIL.getMiniHBaseCluster().getRegionServer(1).getServerName();
089
090    final RegionInfo regionInfo = regionInfos.get(0);
091
092    RSGroupBasedLoadBalancer loadBalancer = master.getLoadBalancer();
093    RSGroupBasedLoadBalancer spiedLoadBalancer = Mockito.spy(loadBalancer);
094    final AtomicReference<RegionPlan> regionPlanRef = new AtomicReference<RegionPlan>();
095
096    /**
097     * Mock {@link RSGroupBasedLoadBalancer#balanceCluster} to return the {@link RegionPlan} to move
098     * the only region to the other RegionServer.
099     */
100    Mockito.doAnswer((InvocationOnMock invocation) -> {
101      @SuppressWarnings("unchecked")
102      Map<TableName, Map<ServerName, List<RegionInfo>>> tableNameToRegionServerNameToRegionInfos =
103        (Map<TableName, Map<ServerName, List<RegionInfo>>>) invocation.getArgument(0);
104      Map<ServerName, List<RegionInfo>> regionServerNameToRegionInfos =
105        tableNameToRegionServerNameToRegionInfos.get(tableName);
106      assertTrue(regionServerNameToRegionInfos.size() == 2);
107      List<ServerName> assignedRegionServerNames = new ArrayList<ServerName>();
108      for (Map.Entry<ServerName, List<RegionInfo>> entry : regionServerNameToRegionInfos
109        .entrySet()) {
110        if (entry.getValue().size() > 0) {
111          assignedRegionServerNames.add(entry.getKey());
112        }
113      }
114      assertTrue(assignedRegionServerNames.size() == 1);
115      ServerName assignedRegionServerName = assignedRegionServerNames.get(0);
116      ServerName notAssignedRegionServerName =
117        assignedRegionServerName.equals(serverName1) ? serverName2 : serverName1;
118      RegionPlan regionPlan =
119        new RegionPlan(regionInfo, assignedRegionServerName, notAssignedRegionServerName);
120      regionPlanRef.set(regionPlan);
121      return Arrays.asList(regionPlan);
122    }).when(spiedLoadBalancer).balanceCluster(Mockito.anyMap());
123
124    AssignmentManager assignmentManager = master.getAssignmentManager();
125    final AssignmentManager spiedAssignmentManager = Mockito.spy(assignmentManager);
126    final CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
127
128    /**
129     * Override {@link AssignmentManager#balance} to invoke real {@link AssignmentManager#balance}
130     * after the region is successfully unassigned.
131     */
132    Mockito.doAnswer((InvocationOnMock invocation) -> {
133      RegionPlan regionPlan = invocation.getArgument(0);
134      RegionPlan referedRegionPlan = regionPlanRef.get();
135      assertTrue(referedRegionPlan != null);
136      if (referedRegionPlan.equals(regionPlan)) {
137        /**
138         * To make {@link AssignmentManager#unassign} could be invoked just before
139         * {@link AssignmentManager#balance} is invoked.
140         */
141        cyclicBarrier.await();
142        /**
143         * After {@link AssignmentManager#unassign} is completed,we could invoke
144         * {@link AssignmentManager#balance}.
145         */
146        cyclicBarrier.await();
147      }
148      /**
149       * Before HBASE-26712,here may throw NPE.
150       */
151      return invocation.callRealMethod();
152    }).when(spiedAssignmentManager).balance(Mockito.any());
153
154    try {
155      final AtomicReference<Throwable> exceptionRef = new AtomicReference<Throwable>(null);
156      Thread unassignThread = new Thread(() -> {
157        try {
158          /**
159           * To invoke {@link AssignmentManager#unassign} just before
160           * {@link AssignmentManager#balance} is invoked.
161           */
162          cyclicBarrier.await();
163          spiedAssignmentManager.unassign(regionInfo);
164          assertTrue(spiedAssignmentManager.getRegionStates().getRegionAssignments().get(regionInfo)
165              == null);
166          /**
167           * After {@link AssignmentManager#unassign} is completed,we could invoke
168           * {@link AssignmentManager#balance}.
169           */
170          cyclicBarrier.await();
171        } catch (Exception e) {
172          exceptionRef.set(e);
173        }
174      });
175      unassignThread.setName("UnassignThread");
176      unassignThread.start();
177
178      master.setLoadBalancer(spiedLoadBalancer);
179      master.setAssignmentManager(spiedAssignmentManager);
180      /**
181       * enable balance
182       */
183      TEST_UTIL.getAdmin().balancerSwitch(true, false);
184      /**
185       * Before HBASE-26712,here invokes {@link AssignmentManager#balance(RegionPlan)} which may
186       * throw NPE.
187       */
188      master.balanceOrUpdateMetrics();
189
190      unassignThread.join();
191      assertTrue(exceptionRef.get() == null);
192    } finally {
193      master.setLoadBalancer(loadBalancer);
194      master.setAssignmentManager(assignmentManager);
195    }
196  }
197
198  private TableName createTable(String table) throws IOException {
199    TableName tableName = TableName.valueOf(table);
200    TEST_UTIL.createTable(tableName, FAMILYNAME);
201    return tableName;
202  }
203
204}