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.normalizer;
019
020import static java.lang.String.format;
021import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.DEFAULT_MERGE_MIN_REGION_AGE_DAYS;
022import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_ENABLED_KEY;
023import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_MIN_REGION_AGE_DAYS_KEY;
024import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_MIN_REGION_COUNT_KEY;
025import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_MIN_REGION_SIZE_MB_KEY;
026import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MIN_REGION_COUNT_KEY;
027import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.SPLIT_ENABLED_KEY;
028import static org.hamcrest.MatcherAssert.assertThat;
029import static org.hamcrest.Matchers.contains;
030import static org.hamcrest.Matchers.empty;
031import static org.hamcrest.Matchers.everyItem;
032import static org.hamcrest.Matchers.greaterThanOrEqualTo;
033import static org.hamcrest.Matchers.instanceOf;
034import static org.hamcrest.Matchers.not;
035import static org.junit.Assert.assertEquals;
036import static org.junit.Assert.assertFalse;
037import static org.junit.Assert.assertTrue;
038import static org.mockito.ArgumentMatchers.any;
039import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
040import static org.mockito.Mockito.when;
041
042import java.time.Instant;
043import java.time.Period;
044import java.util.ArrayList;
045import java.util.Collections;
046import java.util.HashMap;
047import java.util.List;
048import java.util.Map;
049import org.apache.hadoop.conf.Configuration;
050import org.apache.hadoop.hbase.HBaseClassTestRule;
051import org.apache.hadoop.hbase.HBaseConfiguration;
052import org.apache.hadoop.hbase.RegionMetrics;
053import org.apache.hadoop.hbase.ServerName;
054import org.apache.hadoop.hbase.Size;
055import org.apache.hadoop.hbase.TableName;
056import org.apache.hadoop.hbase.TableNameTestRule;
057import org.apache.hadoop.hbase.client.RegionInfo;
058import org.apache.hadoop.hbase.client.RegionInfoBuilder;
059import org.apache.hadoop.hbase.client.TableDescriptor;
060import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
061import org.apache.hadoop.hbase.master.MasterServices;
062import org.apache.hadoop.hbase.master.RegionState;
063import org.apache.hadoop.hbase.testclassification.MasterTests;
064import org.apache.hadoop.hbase.testclassification.SmallTests;
065import org.apache.hadoop.hbase.util.Bytes;
066import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
067import org.junit.Before;
068import org.junit.ClassRule;
069import org.junit.Rule;
070import org.junit.Test;
071import org.junit.experimental.categories.Category;
072import org.mockito.Mockito;
073
074/**
075 * Tests logic of {@link SimpleRegionNormalizer}.
076 */
077@Category({ MasterTests.class, SmallTests.class })
078public class TestSimpleRegionNormalizer {
079
080  @ClassRule
081  public static final HBaseClassTestRule CLASS_RULE =
082    HBaseClassTestRule.forClass(TestSimpleRegionNormalizer.class);
083
084  private Configuration conf;
085  private SimpleRegionNormalizer normalizer;
086  private MasterServices masterServices;
087  private TableDescriptor tableDescriptor;
088
089  @Rule
090  public TableNameTestRule name = new TableNameTestRule();
091
092  @Before
093  public void before() {
094    conf = HBaseConfiguration.create();
095    tableDescriptor = TableDescriptorBuilder.newBuilder(name.getTableName()).build();
096  }
097
098  @Test
099  public void testNoNormalizationForMetaTable() {
100    TableName testTable = TableName.META_TABLE_NAME;
101    TableDescriptor testMetaTd = TableDescriptorBuilder.newBuilder(testTable).build();
102    List<RegionInfo> RegionInfo = new ArrayList<>();
103    Map<byte[], Integer> regionSizes = new HashMap<>();
104
105    setupMocksForNormalizer(regionSizes, RegionInfo);
106    List<NormalizationPlan> plans = normalizer.computePlansForTable(testMetaTd);
107    assertThat(plans, empty());
108  }
109
110  @Test
111  public void testNoNormalizationIfTooFewRegions() {
112    final TableName tableName = name.getTableName();
113    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 2);
114    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 10, 15);
115    setupMocksForNormalizer(regionSizes, regionInfos);
116
117    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
118    assertThat(plans, empty());
119  }
120
121  @Test
122  public void testNoNormalizationOnNormalizedCluster() {
123    final TableName tableName = name.getTableName();
124    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 4);
125    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 10, 15, 8, 10);
126    setupMocksForNormalizer(regionSizes, regionInfos);
127
128    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
129    assertThat(plans, empty());
130  }
131
132  private void noNormalizationOnTransitioningRegions(final RegionState.State state) {
133    final TableName tableName = name.getTableName();
134    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 3);
135    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 10, 1, 100);
136
137    setupMocksForNormalizer(regionSizes, regionInfos);
138    when(
139      masterServices.getAssignmentManager().getRegionStates().getRegionState(any(RegionInfo.class)))
140        .thenReturn(RegionState.createForTesting(null, state));
141    assertThat(normalizer.getMergeMinRegionCount(), greaterThanOrEqualTo(regionInfos.size()));
142
143    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
144    assertThat(format("Unexpected plans for RegionState %s", state), plans, empty());
145  }
146
147  @Test
148  public void testNoNormalizationOnMergingNewRegions() {
149    noNormalizationOnTransitioningRegions(RegionState.State.MERGING_NEW);
150  }
151
152  @Test
153  public void testNoNormalizationOnMergingRegions() {
154    noNormalizationOnTransitioningRegions(RegionState.State.MERGING);
155  }
156
157  @Test
158  public void testNoNormalizationOnMergedRegions() {
159    noNormalizationOnTransitioningRegions(RegionState.State.MERGED);
160  }
161
162  @Test
163  public void testNoNormalizationOnSplittingNewRegions() {
164    noNormalizationOnTransitioningRegions(RegionState.State.SPLITTING_NEW);
165  }
166
167  @Test
168  public void testNoNormalizationOnSplittingRegions() {
169    noNormalizationOnTransitioningRegions(RegionState.State.SPLITTING);
170  }
171
172  @Test
173  public void testNoNormalizationOnSplitRegions() {
174    noNormalizationOnTransitioningRegions(RegionState.State.SPLIT);
175  }
176
177  @Test
178  public void testMergeOfSmallRegions() {
179    final TableName tableName = name.getTableName();
180    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
181    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 15, 5, 5, 15, 16);
182    setupMocksForNormalizer(regionSizes, regionInfos);
183
184    assertThat(normalizer.computePlansForTable(tableDescriptor),
185      contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(1), 5)
186        .addTarget(regionInfos.get(2), 5).build()));
187  }
188
189  // Test for situation illustrated in HBASE-14867
190  @Test
191  public void testMergeOfSecondSmallestRegions() {
192    final TableName tableName = name.getTableName();
193    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 6);
194    final Map<byte[], Integer> regionSizes =
195      createRegionSizesMap(regionInfos, 1, 10000, 10000, 10000, 2700, 2700);
196    setupMocksForNormalizer(regionSizes, regionInfos);
197
198    assertThat(normalizer.computePlansForTable(tableDescriptor),
199      contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(4), 2700)
200        .addTarget(regionInfos.get(5), 2700).build()));
201  }
202
203  @Test
204  public void testMergeOfSmallNonAdjacentRegions() {
205    final TableName tableName = name.getTableName();
206    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
207    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 15, 5, 16, 15, 5);
208    setupMocksForNormalizer(regionSizes, regionInfos);
209
210    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
211    assertThat(plans, empty());
212  }
213
214  @Test
215  public void testSplitOfLargeRegion() {
216    final TableName tableName = name.getTableName();
217    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 4);
218    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 8, 6, 10, 30);
219    setupMocksForNormalizer(regionSizes, regionInfos);
220
221    assertThat(normalizer.computePlansForTable(tableDescriptor),
222      contains(new SplitNormalizationPlan(regionInfos.get(3), 30)));
223  }
224
225  @Test
226  public void testWithTargetRegionSize() throws Exception {
227    final TableName tableName = name.getTableName();
228    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 6);
229    final Map<byte[], Integer> regionSizes =
230      createRegionSizesMap(regionInfos, 20, 40, 60, 80, 100, 120);
231    setupMocksForNormalizer(regionSizes, regionInfos);
232
233    // test when target region size is 20
234    when(tableDescriptor.getNormalizerTargetRegionSize()).thenReturn(20L);
235    assertThat(normalizer.computePlansForTable(tableDescriptor),
236      contains(new SplitNormalizationPlan(regionInfos.get(2), 60),
237        new SplitNormalizationPlan(regionInfos.get(3), 80),
238        new SplitNormalizationPlan(regionInfos.get(4), 100),
239        new SplitNormalizationPlan(regionInfos.get(5), 120)));
240
241    // test when target region size is 200
242    when(tableDescriptor.getNormalizerTargetRegionSize()).thenReturn(200L);
243    assertThat(normalizer.computePlansForTable(tableDescriptor),
244      contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 20)
245        .addTarget(regionInfos.get(1), 40).addTarget(regionInfos.get(2), 60)
246        .addTarget(regionInfos.get(3), 80).build()));
247  }
248
249  @Test
250  public void testSplitWithTargetRegionCount() throws Exception {
251    final TableName tableName = name.getTableName();
252    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 4);
253    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 20, 40, 60, 80);
254    setupMocksForNormalizer(regionSizes, regionInfos);
255
256    // test when target region count is 8
257    when(tableDescriptor.getNormalizerTargetRegionCount()).thenReturn(8);
258    assertThat(normalizer.computePlansForTable(tableDescriptor),
259      contains(new SplitNormalizationPlan(regionInfos.get(2), 60),
260        new SplitNormalizationPlan(regionInfos.get(3), 80)));
261
262    // test when target region count is 3
263    when(tableDescriptor.getNormalizerTargetRegionCount()).thenReturn(3);
264    assertThat(normalizer.computePlansForTable(tableDescriptor),
265      contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 20)
266        .addTarget(regionInfos.get(1), 40).build()));
267  }
268
269  @Test
270  public void testHonorsSplitEnabled() {
271    conf.setBoolean(SPLIT_ENABLED_KEY, true);
272    final TableName tableName = name.getTableName();
273    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
274    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 5, 5, 20, 5, 5);
275    setupMocksForNormalizer(regionSizes, regionInfos);
276    assertThat(normalizer.computePlansForTable(tableDescriptor),
277      contains(instanceOf(SplitNormalizationPlan.class)));
278
279    conf.setBoolean(SPLIT_ENABLED_KEY, false);
280    setupMocksForNormalizer(regionSizes, regionInfos);
281    assertThat(normalizer.computePlansForTable(tableDescriptor), empty());
282  }
283
284  @Test
285  public void testHonorsSplitEnabledInTD() {
286    conf.setBoolean(SPLIT_ENABLED_KEY, true);
287    final TableName tableName = name.getTableName();
288    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
289    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 5, 5, 20, 5, 5);
290    setupMocksForNormalizer(regionSizes, regionInfos);
291    assertThat(normalizer.computePlansForTable(tableDescriptor),
292      contains(instanceOf(SplitNormalizationPlan.class)));
293
294    // When hbase.normalizer.split.enabled is true in configuration, but false in table descriptor
295    when(tableDescriptor.getValue(SPLIT_ENABLED_KEY)).thenReturn("false");
296    assertThat(normalizer.computePlansForTable(tableDescriptor), empty());
297
298    // When hbase.normalizer.split.enabled is false in configuration, but true in table descriptor
299    conf.setBoolean(SPLIT_ENABLED_KEY, false);
300    setupMocksForNormalizer(regionSizes, regionInfos);
301    when(tableDescriptor.getValue(SPLIT_ENABLED_KEY)).thenReturn("true");
302    assertThat(normalizer.computePlansForTable(tableDescriptor),
303      contains(instanceOf(SplitNormalizationPlan.class)));
304  }
305
306  @Test
307  public void testHonorsMergeEnabled() {
308    conf.setBoolean(MERGE_ENABLED_KEY, true);
309    final TableName tableName = name.getTableName();
310    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
311    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 20, 5, 5, 20, 20);
312    setupMocksForNormalizer(regionSizes, regionInfos);
313    assertThat(normalizer.computePlansForTable(tableDescriptor),
314      contains(instanceOf(MergeNormalizationPlan.class)));
315
316    conf.setBoolean(MERGE_ENABLED_KEY, false);
317    setupMocksForNormalizer(regionSizes, regionInfos);
318    assertThat(normalizer.computePlansForTable(tableDescriptor), empty());
319  }
320
321  @Test
322  public void testHonorsMergeEnabledInTD() {
323    conf.setBoolean(MERGE_ENABLED_KEY, true);
324    final TableName tableName = name.getTableName();
325    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
326    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 20, 5, 5, 20, 20);
327    setupMocksForNormalizer(regionSizes, regionInfos);
328    assertThat(normalizer.computePlansForTable(tableDescriptor),
329      contains(instanceOf(MergeNormalizationPlan.class)));
330
331    // When hbase.normalizer.merge.enabled is true in configuration, but false in table descriptor
332    when(tableDescriptor.getValue(MERGE_ENABLED_KEY)).thenReturn("false");
333    assertThat(normalizer.computePlansForTable(tableDescriptor), empty());
334
335    // When hbase.normalizer.merge.enabled is false in configuration, but true in table descriptor
336    conf.setBoolean(MERGE_ENABLED_KEY, false);
337    setupMocksForNormalizer(regionSizes, regionInfos);
338    when(tableDescriptor.getValue(MERGE_ENABLED_KEY)).thenReturn("true");
339    assertThat(normalizer.computePlansForTable(tableDescriptor),
340      contains(instanceOf(MergeNormalizationPlan.class)));
341  }
342
343  @Test
344  public void testHonorsMinimumRegionCount() {
345    honorsMinimumRegionCount(MERGE_MIN_REGION_COUNT_KEY);
346  }
347
348  /**
349   * Test the backward compatibility of the deprecated MIN_REGION_COUNT_KEY configuration.
350   */
351  @Test
352  public void testHonorsOldMinimumRegionCount() {
353    honorsMinimumRegionCount(MIN_REGION_COUNT_KEY);
354  }
355
356  private void honorsMinimumRegionCount(String confKey) {
357    conf.setInt(confKey, 1);
358    final TableName tableName = name.getTableName();
359    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 3);
360    // create a table topology that results in both a merge plan and a split plan. Assert that the
361    // merge is only created when the when the number of table regions is above the region count
362    // threshold, and that the split plan is create in both cases.
363    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 1, 1, 10);
364    setupMocksForNormalizer(regionSizes, regionInfos);
365    assertEquals(1, normalizer.getMergeMinRegionCount());
366
367    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
368    assertThat(plans,
369      contains(new SplitNormalizationPlan(regionInfos.get(2), 10),
370        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 1)
371          .addTarget(regionInfos.get(1), 1).build()));
372
373    // have to call setupMocks again because we don't have dynamic config update on normalizer.
374    conf.setInt(confKey, 4);
375    setupMocksForNormalizer(regionSizes, regionInfos);
376    assertEquals(4, normalizer.getMergeMinRegionCount());
377    assertThat(normalizer.computePlansForTable(tableDescriptor),
378      contains(new SplitNormalizationPlan(regionInfos.get(2), 10)));
379  }
380
381  @Test
382  public void testHonorsMinimumRegionCountInTD() {
383    honorsOldMinimumRegionCountInTD(MERGE_MIN_REGION_COUNT_KEY);
384  }
385
386  /**
387   * Test the backward compatibility of the deprecated MIN_REGION_COUNT_KEY configuration in table
388   * descriptor.
389   */
390  @Test
391  public void testHonorsOldMinimumRegionCountInTD() {
392    honorsOldMinimumRegionCountInTD(MIN_REGION_COUNT_KEY);
393  }
394
395  private void honorsOldMinimumRegionCountInTD(String confKey) {
396    conf.setInt(confKey, 1);
397    final TableName tableName = name.getTableName();
398    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 3);
399    // create a table topology that results in both a merge plan and a split plan. Assert that the
400    // merge is only created when the when the number of table regions is above the region count
401    // threshold, and that the split plan is create in both cases.
402    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 1, 1, 10);
403    setupMocksForNormalizer(regionSizes, regionInfos);
404    assertEquals(1, normalizer.getMergeMinRegionCount());
405
406    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
407    assertThat(plans,
408      contains(new SplitNormalizationPlan(regionInfos.get(2), 10),
409        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 1)
410          .addTarget(regionInfos.get(1), 1).build()));
411
412    when(tableDescriptor.getValue(confKey)).thenReturn("4");
413    assertThat(normalizer.computePlansForTable(tableDescriptor),
414      contains(new SplitNormalizationPlan(regionInfos.get(2), 10)));
415  }
416
417  @Test
418  public void testHonorsMergeMinRegionAge() {
419    conf.setInt(MERGE_MIN_REGION_AGE_DAYS_KEY, 7);
420    final TableName tableName = name.getTableName();
421    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 4);
422    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 1, 1, 10, 10);
423    setupMocksForNormalizer(regionSizes, regionInfos);
424    assertEquals(Period.ofDays(7), normalizer.getMergeMinRegionAge());
425    assertThat(normalizer.computePlansForTable(tableDescriptor),
426      everyItem(not(instanceOf(MergeNormalizationPlan.class))));
427
428    // have to call setupMocks again because we don't have dynamic config update on normalizer.
429    conf.unset(MERGE_MIN_REGION_AGE_DAYS_KEY);
430    setupMocksForNormalizer(regionSizes, regionInfos);
431    assertEquals(Period.ofDays(DEFAULT_MERGE_MIN_REGION_AGE_DAYS),
432      normalizer.getMergeMinRegionAge());
433    final List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
434    assertThat(plans, not(empty()));
435    assertThat(plans, everyItem(instanceOf(MergeNormalizationPlan.class)));
436  }
437
438  @Test
439  public void testHonorsMergeMinRegionAgeInTD() {
440    conf.setInt(MERGE_MIN_REGION_AGE_DAYS_KEY, 7);
441    final TableName tableName = name.getTableName();
442    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 4);
443    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 1, 1, 10, 10);
444    setupMocksForNormalizer(regionSizes, regionInfos);
445    assertEquals(Period.ofDays(7), normalizer.getMergeMinRegionAge());
446    assertThat(normalizer.computePlansForTable(tableDescriptor),
447      everyItem(not(instanceOf(MergeNormalizationPlan.class))));
448
449    conf.unset(MERGE_MIN_REGION_AGE_DAYS_KEY);
450    setupMocksForNormalizer(regionSizes, regionInfos);
451    when(tableDescriptor.getValue(MERGE_MIN_REGION_AGE_DAYS_KEY)).thenReturn("-1");
452    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
453    assertThat(plans, not(empty()));
454    assertThat(plans, everyItem(instanceOf(MergeNormalizationPlan.class)));
455
456    when(tableDescriptor.getValue(MERGE_MIN_REGION_AGE_DAYS_KEY)).thenReturn("5");
457    plans = normalizer.computePlansForTable(tableDescriptor);
458    assertThat(plans, empty());
459    assertThat(plans, everyItem(not(instanceOf(MergeNormalizationPlan.class))));
460  }
461
462  @Test
463  public void testHonorsMergeMinRegionSize() {
464    conf.setBoolean(SPLIT_ENABLED_KEY, false);
465    final TableName tableName = name.getTableName();
466    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
467    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 1, 2, 0, 10, 10);
468    setupMocksForNormalizer(regionSizes, regionInfos);
469
470    assertFalse(normalizer.isSplitEnabled());
471    assertEquals(1, normalizer.getMergeMinRegionSizeMb());
472    assertThat(normalizer.computePlansForTable(tableDescriptor),
473      contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 1)
474        .addTarget(regionInfos.get(1), 2).build()));
475
476    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 3);
477    setupMocksForNormalizer(regionSizes, regionInfos);
478    assertEquals(3, normalizer.getMergeMinRegionSizeMb());
479    assertThat(normalizer.computePlansForTable(tableDescriptor), empty());
480  }
481
482  @Test
483  public void testHonorsMergeMinRegionSizeInTD() {
484    conf.setBoolean(SPLIT_ENABLED_KEY, false);
485    final TableName tableName = name.getTableName();
486    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
487    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 1, 2, 0, 10, 10);
488    setupMocksForNormalizer(regionSizes, regionInfos);
489
490    assertFalse(normalizer.isSplitEnabled());
491    assertEquals(1, normalizer.getMergeMinRegionSizeMb());
492    assertThat(normalizer.computePlansForTable(tableDescriptor),
493      contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 1)
494        .addTarget(regionInfos.get(1), 2).build()));
495
496    when(tableDescriptor.getValue(MERGE_MIN_REGION_SIZE_MB_KEY)).thenReturn("3");
497    assertThat(normalizer.computePlansForTable(tableDescriptor), empty());
498  }
499
500  @Test
501  public void testMergeEmptyRegions0() {
502    conf.setBoolean(SPLIT_ENABLED_KEY, false);
503    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
504    final TableName tableName = name.getTableName();
505    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 7);
506    final Map<byte[], Integer> regionSizes =
507      createRegionSizesMap(regionInfos, 0, 1, 10, 0, 9, 10, 0);
508    setupMocksForNormalizer(regionSizes, regionInfos);
509
510    assertFalse(normalizer.isSplitEnabled());
511    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
512    assertThat(normalizer.computePlansForTable(tableDescriptor),
513      contains(
514        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
515          .addTarget(regionInfos.get(1), 1).build(),
516        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(2), 10)
517          .addTarget(regionInfos.get(3), 0).build(),
518        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(5), 10)
519          .addTarget(regionInfos.get(6), 0).build()));
520  }
521
522  @Test
523  public void testMergeEmptyRegions1() {
524    conf.setBoolean(SPLIT_ENABLED_KEY, false);
525    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
526    final TableName tableName = name.getTableName();
527    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 8);
528    final Map<byte[], Integer> regionSizes =
529      createRegionSizesMap(regionInfos, 0, 1, 10, 0, 9, 0, 10, 0);
530    setupMocksForNormalizer(regionSizes, regionInfos);
531
532    assertFalse(normalizer.isSplitEnabled());
533    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
534    assertThat(normalizer.computePlansForTable(tableDescriptor),
535      contains(
536        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
537          .addTarget(regionInfos.get(1), 1).build(),
538        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(2), 10)
539          .addTarget(regionInfos.get(3), 0).build(),
540        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(4), 9)
541          .addTarget(regionInfos.get(5), 0).build(),
542        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(6), 10)
543          .addTarget(regionInfos.get(7), 0).build()));
544  }
545
546  @Test
547  public void testMergeEmptyRegions2() {
548    conf.setBoolean(SPLIT_ENABLED_KEY, false);
549    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
550    final TableName tableName = name.getTableName();
551    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 8);
552    final Map<byte[], Integer> regionSizes =
553      createRegionSizesMap(regionInfos, 0, 10, 1, 0, 9, 0, 10, 0);
554    setupMocksForNormalizer(regionSizes, regionInfos);
555
556    assertFalse(normalizer.isSplitEnabled());
557    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
558    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
559    assertThat(plans,
560      contains(
561        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
562          .addTarget(regionInfos.get(1), 10).build(),
563        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(2), 1)
564          .addTarget(regionInfos.get(3), 0).build(),
565        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(4), 9)
566          .addTarget(regionInfos.get(5), 0).build(),
567        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(6), 10)
568          .addTarget(regionInfos.get(7), 0).build()));
569  }
570
571  @Test
572  public void testSplitAndMultiMerge() {
573    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
574    final TableName tableName = name.getTableName();
575    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 8);
576    final Map<byte[], Integer> regionSizes =
577      createRegionSizesMap(regionInfos, 3, 1, 1, 30, 9, 3, 1, 0);
578    setupMocksForNormalizer(regionSizes, regionInfos);
579
580    assertTrue(normalizer.isMergeEnabled());
581    assertTrue(normalizer.isSplitEnabled());
582    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
583    assertThat(normalizer.computePlansForTable(tableDescriptor),
584      contains(new SplitNormalizationPlan(regionInfos.get(3), 30),
585        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 3)
586          .addTarget(regionInfos.get(1), 1).addTarget(regionInfos.get(2), 1).build(),
587        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(5), 3)
588          .addTarget(regionInfos.get(6), 1).addTarget(regionInfos.get(7), 0).build()));
589  }
590
591  // This test is to make sure that normalizer is only going to merge adjacent regions.
592  @Test
593  public void testNormalizerCannotMergeNonAdjacentRegions() {
594    final TableName tableName = name.getTableName();
595    // create 5 regions with sizes to trigger merge of small regions. region ranges are:
596    // [, "aa"), ["aa", "aa1"), ["aa1", "aa1!"), ["aa1!", "aa2"), ["aa2", )
597    // Region ["aa", "aa1") and ["aa1!", "aa2") are not adjacent, they are not supposed to
598    // merged.
599    final byte[][] keys = { null, Bytes.toBytes("aa"), Bytes.toBytes("aa1!"), Bytes.toBytes("aa1"),
600      Bytes.toBytes("aa2"), null, };
601    final List<RegionInfo> regionInfos = createRegionInfos(tableName, keys);
602    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 3, 1, 1, 3, 5);
603    setupMocksForNormalizer(regionSizes, regionInfos);
604
605    // Compute the plan, no merge plan returned as they are not adjacent.
606    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
607    assertThat(plans, empty());
608  }
609
610  @SuppressWarnings("MockitoCast")
611  private void setupMocksForNormalizer(Map<byte[], Integer> regionSizes,
612    List<RegionInfo> regionInfoList) {
613    masterServices = Mockito.mock(MasterServices.class, RETURNS_DEEP_STUBS);
614    tableDescriptor = Mockito.mock(TableDescriptor.class, RETURNS_DEEP_STUBS);
615
616    // for simplicity all regions are assumed to be on one server; doesn't matter to us
617    ServerName sn = ServerName.valueOf("localhost", 0, 0L);
618    when(masterServices.getAssignmentManager().getRegionStates().getRegionsOfTable(any()))
619      .thenReturn(regionInfoList);
620    when(masterServices.getAssignmentManager().getRegionStates().getRegionServerOfRegion(any()))
621      .thenReturn(sn);
622    when(
623      masterServices.getAssignmentManager().getRegionStates().getRegionState(any(RegionInfo.class)))
624        .thenReturn(RegionState.createForTesting(null, RegionState.State.OPEN));
625
626    for (Map.Entry<byte[], Integer> region : regionSizes.entrySet()) {
627      RegionMetrics regionLoad = Mockito.mock(RegionMetrics.class);
628      when(regionLoad.getRegionName()).thenReturn(region.getKey());
629      when(regionLoad.getStoreFileSize())
630        .thenReturn(new Size(region.getValue(), Size.Unit.MEGABYTE));
631
632      // this is possibly broken with jdk9, unclear if false positive or not
633      // suppress it for now, fix it when we get to running tests on 9
634      // see: http://errorprone.info/bugpattern/MockitoCast
635      when((Object) masterServices.getServerManager().getLoad(sn).getRegionMetrics()
636        .get(region.getKey())).thenReturn(regionLoad);
637    }
638
639    when(masterServices.isSplitOrMergeEnabled(any())).thenReturn(true);
640    when(tableDescriptor.getTableName()).thenReturn(name.getTableName());
641
642    normalizer = new SimpleRegionNormalizer();
643    normalizer.setConf(conf);
644    normalizer.setMasterServices(masterServices);
645  }
646
647  /**
648   * Create a list of {@link RegionInfo}s that represent a region chain of the specified length.
649   */
650  private static List<RegionInfo> createRegionInfos(final TableName tableName, final int length) {
651    if (length < 1) {
652      throw new IllegalStateException("length must be greater than or equal to 1.");
653    }
654
655    final byte[] startKey = Bytes.toBytes("aaaaa");
656    final byte[] endKey = Bytes.toBytes("zzzzz");
657    if (length == 1) {
658      return Collections.singletonList(createRegionInfo(tableName, startKey, endKey));
659    }
660
661    final byte[][] splitKeys = Bytes.split(startKey, endKey, length - 1);
662    final List<RegionInfo> ret = new ArrayList<>(length);
663    for (int i = 0; i < splitKeys.length - 1; i++) {
664      ret.add(createRegionInfo(tableName, splitKeys[i], splitKeys[i + 1]));
665    }
666    return ret;
667  }
668
669  private static RegionInfo createRegionInfo(final TableName tableName, final byte[] startKey,
670    final byte[] endKey) {
671    return RegionInfoBuilder.newBuilder(tableName).setStartKey(startKey).setEndKey(endKey)
672      .setRegionId(generateRegionId()).build();
673  }
674
675  private static long generateRegionId() {
676    return Instant.ofEpochMilli(EnvironmentEdgeManager.currentTime())
677      .minus(Period.ofDays(DEFAULT_MERGE_MIN_REGION_AGE_DAYS + 1)).toEpochMilli();
678  }
679
680  private static List<RegionInfo> createRegionInfos(final TableName tableName,
681    final byte[][] splitKeys) {
682    final List<RegionInfo> ret = new ArrayList<>(splitKeys.length);
683    for (int i = 0; i < splitKeys.length - 1; i++) {
684      ret.add(createRegionInfo(tableName, splitKeys[i], splitKeys[i + 1]));
685    }
686    return ret;
687  }
688
689  private static Map<byte[], Integer> createRegionSizesMap(final List<RegionInfo> regionInfos,
690    int... sizes) {
691    if (regionInfos.size() != sizes.length) {
692      throw new IllegalStateException("Parameter lengths must match.");
693    }
694
695    final Map<byte[], Integer> ret = new HashMap<>(regionInfos.size());
696    for (int i = 0; i < regionInfos.size(); i++) {
697      ret.put(regionInfos.get(i).getRegionName(), sizes[i]);
698    }
699    return ret;
700  }
701}