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.balancer;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.security.SecureRandom;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.TreeMap;
033import java.util.TreeSet;
034import org.apache.commons.lang3.StringUtils;
035import org.apache.hadoop.hbase.ServerName;
036import org.apache.hadoop.hbase.TableDescriptors;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.client.RegionInfoBuilder;
040import org.apache.hadoop.hbase.client.TableDescriptor;
041import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
042import org.apache.hadoop.hbase.master.HMaster;
043import org.apache.hadoop.hbase.master.MasterServices;
044import org.apache.hadoop.hbase.master.RegionPlan;
045import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
046import org.apache.hadoop.hbase.net.Address;
047import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
048import org.apache.hadoop.hbase.rsgroup.RSGroupInfoManager;
049import org.apache.hadoop.hbase.util.Bytes;
050import org.mockito.Mockito;
051import org.mockito.invocation.InvocationOnMock;
052import org.mockito.stubbing.Answer;
053
054import org.apache.hbase.thirdparty.com.google.common.collect.ArrayListMultimap;
055import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
056
057/**
058 * Base UT of RSGroupableBalancer.
059 */
060public class RSGroupableBalancerTestBase {
061
062  static SecureRandom rand = new SecureRandom();
063  static String[] groups = new String[] {RSGroupInfo.DEFAULT_GROUP, "dg2", "dg3", "dg4"};
064  static TableName table0 = TableName.valueOf("dt0");
065  static TableName[] tables =
066      new TableName[] { TableName.valueOf("dt1"),
067          TableName.valueOf("dt2"),
068          TableName.valueOf("dt3"),
069          TableName.valueOf("dt4")};
070  static List<ServerName> servers;
071  static Map<String, RSGroupInfo> groupMap;
072  static Map<TableName, String> tableMap = new HashMap<>();
073  static List<TableDescriptor> tableDescs;
074  int[] regionAssignment = new int[] { 2, 5, 7, 10, 4, 3, 1 };
075  static int regionId = 0;
076
077  /**
078   * Invariant is that all servers of a group have load between floor(avg) and
079   * ceiling(avg) number of regions.
080   */
081  protected void assertClusterAsBalanced(
082      ArrayListMultimap<String, ServerAndLoad> groupLoadMap) {
083    for (String gName : groupLoadMap.keySet()) {
084      List<ServerAndLoad> groupLoad = groupLoadMap.get(gName);
085      int numServers = groupLoad.size();
086      int numRegions = 0;
087      int maxRegions = 0;
088      int minRegions = Integer.MAX_VALUE;
089      for (ServerAndLoad server : groupLoad) {
090        int nr = server.getLoad();
091        if (nr > maxRegions) {
092          maxRegions = nr;
093        }
094        if (nr < minRegions) {
095          minRegions = nr;
096        }
097        numRegions += nr;
098      }
099      if (maxRegions - minRegions < 2) {
100        // less than 2 between max and min, can't balance
101        return;
102      }
103      int min = numRegions / numServers;
104      int max = numRegions % numServers == 0 ? min : min + 1;
105
106      for (ServerAndLoad server : groupLoad) {
107        assertTrue(server.getLoad() <= max);
108        assertTrue(server.getLoad() >= min);
109      }
110    }
111  }
112
113  /**
114   * All regions have an assignment.
115   */
116  protected void assertImmediateAssignment(List<RegionInfo> regions,
117                                         List<ServerName> servers,
118                                         Map<RegionInfo, ServerName> assignments)
119      throws IOException {
120    for (RegionInfo region : regions) {
121      assertTrue(assignments.containsKey(region));
122      ServerName server = assignments.get(region);
123      TableName tableName = region.getTable();
124
125      String groupName = getMockedGroupInfoManager().getRSGroupOfTable(tableName);
126      assertTrue(StringUtils.isNotEmpty(groupName));
127      RSGroupInfo gInfo = getMockedGroupInfoManager().getRSGroup(groupName);
128      assertTrue("Region is not correctly assigned to group servers.",
129          gInfo.containsServer(server.getAddress()));
130    }
131  }
132
133  /**
134   * Asserts a valid retained assignment plan.
135   * <p>
136   * Must meet the following conditions:
137   * <ul>
138   * <li>Every input region has an assignment, and to an online server
139   * <li>If a region had an existing assignment to a server with the same
140   * address a a currently online server, it will be assigned to it
141   * </ul>
142   */
143  protected void assertRetainedAssignment(
144      Map<RegionInfo, ServerName> existing, List<ServerName> servers,
145      Map<ServerName, List<RegionInfo>> assignment)
146      throws FileNotFoundException, IOException {
147    // Verify condition 1, every region assigned, and to online server
148    Set<ServerName> onlineServerSet = new TreeSet<>(servers);
149    Set<RegionInfo> assignedRegions = new TreeSet<>(RegionInfo.COMPARATOR);
150    for (Map.Entry<ServerName, List<RegionInfo>> a : assignment.entrySet()) {
151      assertTrue(
152          "Region assigned to server that was not listed as online",
153          onlineServerSet.contains(a.getKey()));
154      for (RegionInfo r : a.getValue()) {
155        assignedRegions.add(r);
156      }
157    }
158    assertEquals(existing.size(), assignedRegions.size());
159
160    // Verify condition 2, every region must be assigned to correct server.
161    Set<String> onlineHostNames = new TreeSet<>();
162    for (ServerName s : servers) {
163      onlineHostNames.add(s.getHostname());
164    }
165
166    for (Map.Entry<ServerName, List<RegionInfo>> a : assignment.entrySet()) {
167      ServerName currentServer = a.getKey();
168      for (RegionInfo r : a.getValue()) {
169        ServerName oldAssignedServer = existing.get(r);
170        TableName tableName = r.getTable();
171        String groupName =
172            getMockedGroupInfoManager().getRSGroupOfTable(tableName);
173        assertTrue(StringUtils.isNotEmpty(groupName));
174        RSGroupInfo gInfo = getMockedGroupInfoManager().getRSGroup(
175            groupName);
176        assertTrue(
177            "Region is not correctly assigned to group servers.",
178            gInfo.containsServer(currentServer.getAddress()));
179        if (oldAssignedServer != null
180            && onlineHostNames.contains(oldAssignedServer
181            .getHostname())) {
182          // this region was previously assigned somewhere, and that
183          // host is still around, then the host must have been is a
184          // different group.
185          if (!oldAssignedServer.getAddress().equals(currentServer.getAddress())) {
186            assertFalse(gInfo.containsServer(oldAssignedServer.getAddress()));
187          }
188        }
189      }
190    }
191  }
192
193  protected String printStats(
194      ArrayListMultimap<String, ServerAndLoad> groupBasedLoad) {
195    StringBuilder sb = new StringBuilder();
196    sb.append("\n");
197    for (String groupName : groupBasedLoad.keySet()) {
198      sb.append("Stats for group: " + groupName);
199      sb.append("\n");
200      sb.append(groupMap.get(groupName).getServers());
201      sb.append("\n");
202      List<ServerAndLoad> groupLoad = groupBasedLoad.get(groupName);
203      int numServers = groupLoad.size();
204      int totalRegions = 0;
205      sb.append("Per Server Load: \n");
206      for (ServerAndLoad sLoad : groupLoad) {
207        sb.append("Server :" + sLoad.getServerName() + " Load : "
208            + sLoad.getLoad() + "\n");
209        totalRegions += sLoad.getLoad();
210      }
211      sb.append(" Group Statistics : \n");
212      float average = (float) totalRegions / numServers;
213      int max = (int) Math.ceil(average);
214      int min = (int) Math.floor(average);
215      sb.append("[srvr=" + numServers + " rgns=" + totalRegions + " avg="
216          + average + " max=" + max + " min=" + min + "]");
217      sb.append("\n");
218      sb.append("===============================");
219      sb.append("\n");
220    }
221    return sb.toString();
222  }
223
224  protected ArrayListMultimap<String, ServerAndLoad> convertToGroupBasedMap(
225      final Map<ServerName, List<RegionInfo>> serversMap) throws IOException {
226    ArrayListMultimap<String, ServerAndLoad> loadMap = ArrayListMultimap
227        .create();
228    for (RSGroupInfo gInfo : getMockedGroupInfoManager().listRSGroups()) {
229      Set<Address> groupServers = gInfo.getServers();
230      for (Address hostPort : groupServers) {
231        ServerName actual = null;
232        for(ServerName entry: servers) {
233          if(entry.getAddress().equals(hostPort)) {
234            actual = entry;
235            break;
236          }
237        }
238        List<RegionInfo> regions = serversMap.get(actual);
239        assertTrue("No load for " + actual, regions != null);
240        loadMap.put(gInfo.getName(),
241            new ServerAndLoad(actual, regions.size()));
242      }
243    }
244    return loadMap;
245  }
246
247  protected ArrayListMultimap<String, ServerAndLoad> reconcile(
248      ArrayListMultimap<String, ServerAndLoad> previousLoad,
249      List<RegionPlan> plans) {
250    ArrayListMultimap<String, ServerAndLoad> result = ArrayListMultimap
251        .create();
252    result.putAll(previousLoad);
253    if (plans != null) {
254      for (RegionPlan plan : plans) {
255        ServerName source = plan.getSource();
256        updateLoad(result, source, -1);
257        ServerName destination = plan.getDestination();
258        updateLoad(result, destination, +1);
259      }
260    }
261    return result;
262  }
263
264  protected void updateLoad(
265      ArrayListMultimap<String, ServerAndLoad> previousLoad,
266      final ServerName sn, final int diff) {
267    for (String groupName : previousLoad.keySet()) {
268      ServerAndLoad newSAL = null;
269      ServerAndLoad oldSAL = null;
270      for (ServerAndLoad sal : previousLoad.get(groupName)) {
271        if (ServerName.isSameAddress(sn, sal.getServerName())) {
272          oldSAL = sal;
273          newSAL = new ServerAndLoad(sn, sal.getLoad() + diff);
274          break;
275        }
276      }
277      if (newSAL != null) {
278        previousLoad.remove(groupName, oldSAL);
279        previousLoad.put(groupName, newSAL);
280        break;
281      }
282    }
283  }
284
285  protected Map<ServerName, List<RegionInfo>> mockClusterServers() throws IOException {
286    assertTrue(servers.size() == regionAssignment.length);
287    Map<ServerName, List<RegionInfo>> assignment = new TreeMap<>();
288    for (int i = 0; i < servers.size(); i++) {
289      int numRegions = regionAssignment[i];
290      List<RegionInfo> regions = assignedRegions(numRegions, servers.get(i));
291      assignment.put(servers.get(i), regions);
292    }
293    return assignment;
294  }
295
296  /**
297   * Generate a list of regions evenly distributed between the tables.
298   *
299   * @param numRegions The number of regions to be generated.
300   * @return List of RegionInfo.
301   */
302  protected List<RegionInfo> randomRegions(int numRegions) {
303    List<RegionInfo> regions = new ArrayList<>(numRegions);
304    byte[] start = new byte[16];
305    byte[] end = new byte[16];
306    rand.nextBytes(start);
307    rand.nextBytes(end);
308    int regionIdx = rand.nextInt(tables.length);
309    for (int i = 0; i < numRegions; i++) {
310      Bytes.putInt(start, 0, numRegions << 1);
311      Bytes.putInt(end, 0, (numRegions << 1) + 1);
312      int tableIndex = (i + regionIdx) % tables.length;
313      regions.add(RegionInfoBuilder.newBuilder(tables[tableIndex])
314          .setStartKey(start)
315          .setEndKey(end)
316          .setSplit(false)
317          .setRegionId(regionId++)
318          .build());
319    }
320    return regions;
321  }
322
323  /**
324   * Generate assigned regions to a given server using group information.
325   *
326   * @param numRegions the num regions to generate
327   * @param sn the servername
328   * @return the list of regions
329   * @throws java.io.IOException Signals that an I/O exception has occurred.
330   */
331  protected List<RegionInfo> assignedRegions(int numRegions, ServerName sn) throws IOException {
332    List<RegionInfo> regions = new ArrayList<>(numRegions);
333    byte[] start = new byte[16];
334    byte[] end = new byte[16];
335    Bytes.putInt(start, 0, numRegions << 1);
336    Bytes.putInt(end, 0, (numRegions << 1) + 1);
337    for (int i = 0; i < numRegions; i++) {
338      TableName tableName = getTableName(sn);
339      regions.add(RegionInfoBuilder.newBuilder(tableName)
340          .setStartKey(start)
341          .setEndKey(end)
342          .setSplit(false)
343          .setRegionId(regionId++)
344          .build());
345    }
346    return regions;
347  }
348
349  protected static List<ServerName> generateServers(int numServers) {
350    List<ServerName> servers = new ArrayList<>(numServers);
351    for (int i = 0; i < numServers; i++) {
352      String host = "server" + rand.nextInt(100000);
353      int port = rand.nextInt(60000);
354      servers.add(ServerName.valueOf(host, port, -1));
355    }
356    return servers;
357  }
358
359  /**
360   * Construct group info, with each group having at least one server.
361   *
362   * @param servers the servers
363   * @param groups the groups
364   * @return the map
365   */
366  protected static Map<String, RSGroupInfo> constructGroupInfo(
367      List<ServerName> servers, String[] groups) {
368    assertTrue(servers != null);
369    assertTrue(servers.size() >= groups.length);
370    int index = 0;
371    Map<String, RSGroupInfo> groupMap = new HashMap<>();
372    for (String grpName : groups) {
373      RSGroupInfo RSGroupInfo = new RSGroupInfo(grpName);
374      RSGroupInfo.addServer(servers.get(index).getAddress());
375      groupMap.put(grpName, RSGroupInfo);
376      index++;
377    }
378    while (index < servers.size()) {
379      int grpIndex = rand.nextInt(groups.length);
380      groupMap.get(groups[grpIndex]).addServer(
381          servers.get(index).getAddress());
382      index++;
383    }
384    return groupMap;
385  }
386
387  /**
388   * Construct table descriptors evenly distributed between the groups.
389   * @param hasBogusTable there is a table that does not determine the group
390   * @return the list of table descriptors
391   */
392  protected static List<TableDescriptor> constructTableDesc(boolean hasBogusTable) {
393    List<TableDescriptor> tds = Lists.newArrayList();
394    int index = rand.nextInt(groups.length);
395    for (int i = 0; i < tables.length; i++) {
396      TableDescriptor htd = TableDescriptorBuilder.newBuilder(tables[i]).build();
397      int grpIndex = (i + index) % groups.length;
398      String groupName = groups[grpIndex];
399      tableMap.put(tables[i], groupName);
400      tds.add(htd);
401    }
402    if (hasBogusTable) {
403      tableMap.put(table0, "");
404      tds.add(TableDescriptorBuilder.newBuilder(table0).build());
405    }
406    return tds;
407  }
408
409  protected static MasterServices getMockedMaster() throws IOException {
410    TableDescriptors tds = Mockito.mock(TableDescriptors.class);
411    Mockito.when(tds.get(tables[0])).thenReturn(tableDescs.get(0));
412    Mockito.when(tds.get(tables[1])).thenReturn(tableDescs.get(1));
413    Mockito.when(tds.get(tables[2])).thenReturn(tableDescs.get(2));
414    Mockito.when(tds.get(tables[3])).thenReturn(tableDescs.get(3));
415    MasterServices services = Mockito.mock(HMaster.class);
416    Mockito.when(services.getTableDescriptors()).thenReturn(tds);
417    AssignmentManager am = Mockito.mock(AssignmentManager.class);
418    Mockito.when(services.getAssignmentManager()).thenReturn(am);
419    return services;
420  }
421
422  protected static RSGroupInfoManager getMockedGroupInfoManager() throws IOException {
423    RSGroupInfoManager gm = Mockito.mock(RSGroupInfoManager.class);
424    Mockito.when(gm.getRSGroup(Mockito.any())).thenAnswer(new Answer<RSGroupInfo>() {
425      @Override
426      public RSGroupInfo answer(InvocationOnMock invocation) throws Throwable {
427        return groupMap.get(invocation.getArgument(0));
428      }
429    });
430    Mockito.when(gm.listRSGroups()).thenReturn(
431        Lists.newLinkedList(groupMap.values()));
432    Mockito.when(gm.isOnline()).thenReturn(true);
433    Mockito.when(gm.getRSGroupOfTable(Mockito.any()))
434        .thenAnswer(new Answer<String>() {
435          @Override
436          public String answer(InvocationOnMock invocation) throws Throwable {
437            return tableMap.get(invocation.getArgument(0));
438          }
439        });
440    return gm;
441  }
442
443  protected TableName getTableName(ServerName sn) throws IOException {
444    TableName tableName = null;
445    RSGroupInfoManager gm = getMockedGroupInfoManager();
446    RSGroupInfo groupOfServer = null;
447    for(RSGroupInfo gInfo : gm.listRSGroups()){
448      if(gInfo.containsServer(sn.getAddress())){
449        groupOfServer = gInfo;
450        break;
451      }
452    }
453
454    for(TableDescriptor desc : tableDescs){
455      if(gm.getRSGroupOfTable(desc.getTableName()).endsWith(groupOfServer.getName())){
456        tableName = desc.getTableName();
457      }
458    }
459    return tableName;
460  }
461}