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.client;
019
020import static org.apache.hadoop.hbase.favored.FavoredNodesPlan.Position.PRIMARY;
021import static org.apache.hadoop.hbase.favored.FavoredNodesPlan.Position.SECONDARY;
022import static org.apache.hadoop.hbase.favored.FavoredNodesPlan.Position.TERTIARY;
023import static org.junit.jupiter.api.Assertions.assertArrayEquals;
024import static org.junit.jupiter.api.Assertions.assertEquals;
025import static org.junit.jupiter.api.Assertions.assertNotNull;
026import static org.junit.jupiter.api.Assertions.assertNull;
027import static org.junit.jupiter.api.Assertions.assertTrue;
028
029import java.io.IOException;
030import java.net.InetSocketAddress;
031import java.util.List;
032import java.util.Map;
033import java.util.concurrent.TimeUnit;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.hbase.HBaseTestingUtil;
036import org.apache.hadoop.hbase.HConstants;
037import org.apache.hadoop.hbase.HRegionLocation;
038import org.apache.hadoop.hbase.NamespaceDescriptor;
039import org.apache.hadoop.hbase.ServerName;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.Waiter;
042import org.apache.hadoop.hbase.favored.FavoredNodeAssignmentHelper;
043import org.apache.hadoop.hbase.favored.FavoredNodesManager;
044import org.apache.hadoop.hbase.master.LoadBalancer;
045import org.apache.hadoop.hbase.master.ServerManager;
046import org.apache.hadoop.hbase.master.balancer.LoadOnlyFavoredStochasticBalancer;
047import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
048import org.apache.hadoop.hbase.regionserver.HRegionServer;
049import org.apache.hadoop.hbase.testclassification.ClientTests;
050import org.apache.hadoop.hbase.testclassification.MediumTests;
051import org.apache.hadoop.hbase.util.Bytes;
052import org.apache.hadoop.hbase.util.JVMClusterUtil;
053import org.apache.hadoop.hbase.util.Threads;
054import org.junit.jupiter.api.AfterAll;
055import org.junit.jupiter.api.BeforeAll;
056import org.junit.jupiter.api.BeforeEach;
057import org.junit.jupiter.api.Tag;
058import org.junit.jupiter.api.Test;
059import org.junit.jupiter.api.TestInfo;
060import org.slf4j.Logger;
061import org.slf4j.LoggerFactory;
062
063import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
064import org.apache.hbase.thirdparty.com.google.common.collect.Maps;
065
066@Tag(ClientTests.TAG)
067@Tag(MediumTests.TAG)
068public class TestTableFavoredNodes {
069
070  private static final Logger LOG = LoggerFactory.getLogger(TestTableFavoredNodes.class);
071
072  private final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
073  private final static int WAIT_TIMEOUT = 60000;
074  private final static int SLAVES = 8;
075  private FavoredNodesManager fnm;
076  private Admin admin;
077
078  private final byte[][] splitKeys = new byte[][] { Bytes.toBytes(1), Bytes.toBytes(9) };
079
080  private String methodName;
081
082  @BeforeEach
083  public void setupMethodName(TestInfo testInfo) {
084    this.methodName = testInfo.getTestMethod().get().getName();
085  }
086
087  @BeforeAll
088  public static void setupBeforeClass() throws Exception {
089    Configuration conf = TEST_UTIL.getConfiguration();
090    // Setting FavoredNodeBalancer will enable favored nodes
091    conf.setClass(HConstants.HBASE_MASTER_LOADBALANCER_CLASS,
092      LoadOnlyFavoredStochasticBalancer.class, LoadBalancer.class);
093    conf.set(ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, "" + SLAVES);
094
095    TEST_UTIL.startMiniCluster(SLAVES);
096    TEST_UTIL.getMiniHBaseCluster().waitForActiveAndReadyMaster(WAIT_TIMEOUT);
097  }
098
099  @AfterAll
100  public static void tearDownAfterClass() throws Exception {
101    TEST_UTIL.shutdownMiniCluster();
102    TEST_UTIL.cleanupTestDir();
103  }
104
105  @BeforeEach
106  public void setup() throws IOException {
107    fnm = TEST_UTIL.getMiniHBaseCluster().getMaster().getFavoredNodesManager();
108    admin = TEST_UTIL.getAdmin();
109    admin.balancerSwitch(false, true);
110    admin.catalogJanitorSwitch(false);
111  }
112
113  /*
114   * Create a table with FN enabled and check if all its regions have favored nodes set.
115   */
116  @Test
117  public void testCreateTable() throws Exception {
118    final TableName tableName = TableName.valueOf(methodName);
119    TEST_UTIL.createTable(tableName, Bytes.toBytes("f"), splitKeys);
120    TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
121
122    // All regions should have favored nodes
123    checkIfFavoredNodeInformationIsCorrect(tableName);
124
125    List<RegionInfo> regions = admin.getRegions(tableName);
126
127    TEST_UTIL.deleteTable(tableName);
128
129    checkNoFNForDeletedTable(regions);
130  }
131
132  /*
133   * Checks if favored node information is removed on table truncation.
134   */
135  @Test
136  public void testTruncateTable() throws Exception {
137    final TableName tableName = TableName.valueOf(methodName);
138    TEST_UTIL.createTable(tableName, Bytes.toBytes("f"), splitKeys);
139    TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
140
141    // All regions should have favored nodes
142    checkIfFavoredNodeInformationIsCorrect(tableName);
143
144    List<RegionInfo> regions = admin.getRegions(tableName);
145    TEST_UTIL.truncateTable(tableName, true);
146
147    checkNoFNForDeletedTable(regions);
148    checkIfFavoredNodeInformationIsCorrect(tableName);
149
150    regions = admin.getRegions(tableName);
151    TEST_UTIL.truncateTable(tableName, false);
152    checkNoFNForDeletedTable(regions);
153
154    TEST_UTIL.deleteTable(tableName);
155  }
156
157  /*
158   * Check if daughters inherit at-least 2 FN from parent after region split.
159   */
160  @Test
161  public void testSplitTable() throws Exception {
162    final TableName tableName = TableName.valueOf(methodName);
163    Table t = TEST_UTIL.createTable(tableName, Bytes.toBytes("f"), splitKeys);
164    TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
165    final int numberOfRegions = admin.getRegions(t.getName()).size();
166
167    checkIfFavoredNodeInformationIsCorrect(tableName);
168
169    byte[] splitPoint = Bytes.toBytes(0);
170    RegionLocator locator = TEST_UTIL.getConnection().getRegionLocator(tableName);
171    RegionInfo parent = locator.getRegionLocation(splitPoint).getRegion();
172    List<ServerName> parentFN = fnm.getFavoredNodes(parent);
173    assertNotNull(parentFN, "FN should not be null for region: " + parent);
174
175    LOG.info("SPLITTING TABLE");
176    admin.split(tableName, splitPoint);
177
178    TEST_UTIL.waitUntilNoRegionsInTransition(WAIT_TIMEOUT);
179    LOG.info("FINISHED WAITING ON RIT");
180    waitUntilTableRegionCountReached(tableName, numberOfRegions + 1);
181
182    // All regions should have favored nodes checkIfFavoredNodeInformationIsCorrect(tableName);
183
184    // Get the daughters of parent.
185    RegionInfo daughter1 = locator.getRegionLocation(parent.getStartKey(), true).getRegion();
186    List<ServerName> daughter1FN = fnm.getFavoredNodes(daughter1);
187
188    RegionInfo daughter2 = locator.getRegionLocation(splitPoint, true).getRegion();
189    List<ServerName> daughter2FN = fnm.getFavoredNodes(daughter2);
190
191    checkIfDaughterInherits2FN(parentFN, daughter1FN);
192    checkIfDaughterInherits2FN(parentFN, daughter2FN);
193
194    assertEquals(parentFN.get(PRIMARY.ordinal()), daughter1FN.get(PRIMARY.ordinal()),
195      "Daughter's PRIMARY FN should be PRIMARY of parent");
196    assertEquals(parentFN.get(SECONDARY.ordinal()), daughter1FN.get(SECONDARY.ordinal()),
197      "Daughter's SECONDARY FN should be SECONDARY of parent");
198
199    assertEquals(parentFN.get(PRIMARY.ordinal()), daughter2FN.get(PRIMARY.ordinal()),
200      "Daughter's PRIMARY FN should be PRIMARY of parent");
201    assertEquals(parentFN.get(TERTIARY.ordinal()), daughter2FN.get(SECONDARY.ordinal()),
202      "Daughter's SECONDARY FN should be TERTIARY of parent");
203
204    // Major compact table and run catalog janitor. Parent's FN should be removed
205    TEST_UTIL.getMiniHBaseCluster().compact(tableName, true);
206    admin.runCatalogJanitor();
207    // Catalog cleanup is async. Wait on procedure to finish up.
208    ProcedureTestingUtility
209      .waitAllProcedures(TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterProcedureExecutor());
210    // assertEquals("Parent region should have been cleaned", 1, admin.runCatalogScan());
211    assertNull(fnm.getFavoredNodes(parent), "Parent FN should be null");
212
213    List<RegionInfo> regions = admin.getRegions(tableName);
214    // Split and Table Disable interfere with each other around region replicas
215    // TODO. Meantime pause a few seconds.
216    Threads.sleep(2000);
217    LOG.info("STARTING DELETE");
218    TEST_UTIL.deleteTable(tableName);
219
220    checkNoFNForDeletedTable(regions);
221  }
222
223  /*
224   * Check if merged region inherits FN from one of its regions.
225   */
226  @Test
227  public void testMergeTable() throws Exception {
228    final TableName tableName = TableName.valueOf(methodName);
229    TEST_UTIL.createTable(tableName, Bytes.toBytes("f"), splitKeys);
230    TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
231
232    checkIfFavoredNodeInformationIsCorrect(tableName);
233
234    RegionLocator locator = TEST_UTIL.getConnection().getRegionLocator(tableName);
235    RegionInfo regionA = locator.getRegionLocation(HConstants.EMPTY_START_ROW).getRegion();
236    RegionInfo regionB = locator.getRegionLocation(splitKeys[0]).getRegion();
237
238    List<ServerName> regionAFN = fnm.getFavoredNodes(regionA);
239    LOG.info("regionA: " + regionA.getEncodedName() + " with FN: " + fnm.getFavoredNodes(regionA));
240    LOG.info("regionB: " + regionA.getEncodedName() + " with FN: " + fnm.getFavoredNodes(regionB));
241
242    int countOfRegions = TEST_UTIL.getMiniHBaseCluster().getRegions(tableName).size();
243    admin.mergeRegionsAsync(regionA.getEncodedNameAsBytes(), regionB.getEncodedNameAsBytes(), false)
244      .get(60, TimeUnit.SECONDS);
245
246    TEST_UTIL.waitUntilNoRegionsInTransition(WAIT_TIMEOUT);
247    waitUntilTableRegionCountReached(tableName, countOfRegions - 1);
248
249    // All regions should have favored nodes
250    checkIfFavoredNodeInformationIsCorrect(tableName);
251
252    RegionInfo mergedRegion = locator.getRegionLocation(HConstants.EMPTY_START_ROW).getRegion();
253    List<ServerName> mergedFN = fnm.getFavoredNodes(mergedRegion);
254
255    assertArrayEquals(regionAFN.toArray(), mergedFN.toArray(),
256      "Merged region doesn't match regionA's FN");
257
258    // Major compact table and run catalog janitor. Parent FN should be removed
259    TEST_UTIL.getMiniHBaseCluster().compact(tableName, true);
260    assertEquals(1, admin.runCatalogJanitor(), "Merge parents should have been cleaned");
261    // Catalog cleanup is async. Wait on procedure to finish up.
262    ProcedureTestingUtility
263      .waitAllProcedures(TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterProcedureExecutor());
264    assertNull(fnm.getFavoredNodes(regionA), "Parent FN should be null");
265    assertNull(fnm.getFavoredNodes(regionB), "Parent FN should be null");
266
267    List<RegionInfo> regions = admin.getRegions(tableName);
268
269    TEST_UTIL.deleteTable(tableName);
270
271    checkNoFNForDeletedTable(regions);
272  }
273
274  private void checkNoFNForDeletedTable(List<RegionInfo> regions) {
275    for (RegionInfo region : regions) {
276      LOG.info("Testing if FN data for " + region);
277      assertNull(fnm.getFavoredNodes(region), "FN not null for deleted table's region: " + region);
278    }
279  }
280
281  /*
282   * This checks the following: 1. Do all regions of the table have favored nodes updated in master?
283   * 2. Is the number of favored nodes correct for a region? Is the start code -1? 3. Is the FN
284   * information consistent between Master and the respective RegionServer?
285   */
286  private void checkIfFavoredNodeInformationIsCorrect(TableName tableName) throws Exception {
287    /*
288     * Since we need HRegionServer to check for consistency of FN between Master and RS, lets
289     * construct a map for each serverName lookup. Makes it easy later.
290     */
291    Map<ServerName, HRegionServer> snRSMap = Maps.newHashMap();
292    for (JVMClusterUtil.RegionServerThread rst : TEST_UTIL.getMiniHBaseCluster()
293      .getLiveRegionServerThreads()) {
294      snRSMap.put(rst.getRegionServer().getServerName(), rst.getRegionServer());
295    }
296
297    int dnPort = FavoredNodeAssignmentHelper.getDataNodePort(TEST_UTIL.getConfiguration());
298    RegionLocator regionLocator = admin.getConnection().getRegionLocator(tableName);
299    for (HRegionLocation regionLocation : regionLocator.getAllRegionLocations()) {
300
301      RegionInfo regionInfo = regionLocation.getRegion();
302      List<ServerName> fnList = fnm.getFavoredNodes(regionInfo);
303
304      // 1. Does each region have favored node?
305      assertNotNull(fnList, "Favored nodes should not be null for region:" + regionInfo);
306
307      // 2. Do we have the right number of favored nodes? Is start code -1?
308      assertEquals(FavoredNodeAssignmentHelper.FAVORED_NODES_NUM, fnList.size(),
309        "Incorrect favored nodes for region:" + regionInfo + " fnlist: " + fnList);
310      for (ServerName sn : fnList) {
311        assertEquals(-1, sn.getStartCode(), "FN should not have startCode, fnlist:" + fnList);
312      }
313
314      // 3. Check if the regionServers have all the FN updated and in sync with Master
315      HRegionServer regionServer = snRSMap.get(regionLocation.getServerName());
316      assertNotNull(regionServer, "RS should not be null for regionLocation: " + regionLocation);
317
318      InetSocketAddress[] rsFavNodes =
319        regionServer.getFavoredNodesForRegion(regionInfo.getEncodedName());
320      assertNotNull(rsFavNodes,
321        "RS " + regionLocation.getServerName() + " does not have FN for region: " + regionInfo);
322      assertEquals(FavoredNodeAssignmentHelper.FAVORED_NODES_NUM, rsFavNodes.length,
323        "Incorrect FN for region:" + regionInfo.getEncodedName() + " on server:"
324          + regionLocation.getServerName());
325
326      // 4. Does DN port match all FN node list?
327      for (ServerName sn : fnm.getFavoredNodesWithDNPort(regionInfo)) {
328        assertEquals(-1, sn.getStartCode(), "FN should not have startCode, fnlist:" + fnList);
329        assertEquals(dnPort, sn.getPort(), "FN port should belong to DN port, fnlist:" + fnList);
330      }
331    }
332  }
333
334  /*
335   * Check favored nodes for system tables
336   */
337  @Test
338  public void testSystemTables() throws Exception {
339    final TableName tableName = TableName.valueOf(methodName);
340    TEST_UTIL.createTable(tableName, Bytes.toBytes("f"), splitKeys);
341    TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
342
343    // All regions should have favored nodes
344    checkIfFavoredNodeInformationIsCorrect(tableName);
345
346    for (TableName sysTable : admin
347      .listTableNamesByNamespace(NamespaceDescriptor.SYSTEM_NAMESPACE_NAME_STR)) {
348      List<RegionInfo> regions = admin.getRegions(sysTable);
349      for (RegionInfo region : regions) {
350        assertNull(fnm.getFavoredNodes(region), "FN should be null for sys region");
351      }
352    }
353
354    TEST_UTIL.deleteTable(tableName);
355  }
356
357  private void checkIfDaughterInherits2FN(List<ServerName> parentFN, List<ServerName> daughterFN) {
358
359    assertNotNull(parentFN);
360    assertNotNull(daughterFN);
361
362    List<ServerName> favoredNodes = Lists.newArrayList(daughterFN);
363    favoredNodes.removeAll(parentFN);
364
365    /*
366     * With a small cluster its likely some FN might accidentally get shared. Its likely the 3rd FN
367     * the balancer chooses might still belong to the parent in which case favoredNodes size would
368     * be 0.
369     */
370    assertTrue(favoredNodes.size() <= 1,
371      "Daughter FN:" + daughterFN + " should have inherited 2 FN from parent FN:" + parentFN);
372  }
373
374  private void waitUntilTableRegionCountReached(final TableName tableName, final int numRegions)
375    throws Exception {
376    TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate<Exception>() {
377      @Override
378      public boolean evaluate() throws Exception {
379        try (RegionLocator locator = TEST_UTIL.getConnection().getRegionLocator(tableName)) {
380          return locator.getAllRegionLocations().size() == numRegions;
381        }
382      }
383    });
384  }
385}