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.RegionNormalizerWorker.CUMULATIVE_SIZE_LIMIT_MB_KEY;
022import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.DEFAULT_MERGE_MIN_REGION_AGE_DAYS;
023import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_ENABLED_KEY;
024import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_MIN_REGION_AGE_DAYS_KEY;
025import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_MIN_REGION_COUNT_KEY;
026import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_MIN_REGION_SIZE_MB_KEY;
027import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MERGE_REQUEST_MAX_NUMBER_OF_REGIONS_COUNT_KEY;
028import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.MIN_REGION_COUNT_KEY;
029import static org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer.SPLIT_ENABLED_KEY;
030import static org.hamcrest.MatcherAssert.assertThat;
031import static org.hamcrest.Matchers.contains;
032import static org.hamcrest.Matchers.empty;
033import static org.hamcrest.Matchers.everyItem;
034import static org.hamcrest.Matchers.greaterThanOrEqualTo;
035import static org.hamcrest.Matchers.hasSize;
036import static org.hamcrest.Matchers.instanceOf;
037import static org.hamcrest.Matchers.not;
038import static org.junit.jupiter.api.Assertions.assertEquals;
039import static org.junit.jupiter.api.Assertions.assertFalse;
040import static org.junit.jupiter.api.Assertions.assertTrue;
041import static org.mockito.ArgumentMatchers.any;
042import static org.mockito.ArgumentMatchers.anyList;
043import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
044import static org.mockito.Mockito.spy;
045import static org.mockito.Mockito.times;
046import static org.mockito.Mockito.verify;
047import static org.mockito.Mockito.when;
048
049import java.time.Instant;
050import java.time.Period;
051import java.util.ArrayList;
052import java.util.Collections;
053import java.util.HashMap;
054import java.util.List;
055import java.util.Map;
056import org.apache.hadoop.conf.Configuration;
057import org.apache.hadoop.hbase.HBaseConfiguration;
058import org.apache.hadoop.hbase.RegionMetrics;
059import org.apache.hadoop.hbase.ServerName;
060import org.apache.hadoop.hbase.Size;
061import org.apache.hadoop.hbase.TableName;
062import org.apache.hadoop.hbase.client.RegionInfo;
063import org.apache.hadoop.hbase.client.RegionInfoBuilder;
064import org.apache.hadoop.hbase.client.TableDescriptor;
065import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
066import org.apache.hadoop.hbase.master.MasterServices;
067import org.apache.hadoop.hbase.master.RegionState;
068import org.apache.hadoop.hbase.testclassification.MasterTests;
069import org.apache.hadoop.hbase.testclassification.SmallTests;
070import org.apache.hadoop.hbase.util.Bytes;
071import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
072import org.junit.jupiter.api.BeforeEach;
073import org.junit.jupiter.api.Tag;
074import org.junit.jupiter.api.Test;
075import org.junit.jupiter.api.TestInfo;
076import org.mockito.Mockito;
077
078/**
079 * Tests logic of {@link SimpleRegionNormalizer}.
080 */
081@Tag(MasterTests.TAG)
082@Tag(SmallTests.TAG)
083public class TestSimpleRegionNormalizer {
084
085  private Configuration conf;
086  private SimpleRegionNormalizer normalizer;
087  private MasterServices masterServices;
088  private TableDescriptor tableDescriptor;
089  private TableName tableName;
090
091  @BeforeEach
092  public void before(TestInfo testInfo) {
093    conf = HBaseConfiguration.create();
094    tableName = TableName.valueOf(testInfo.getTestMethod().get().getName());
095    tableDescriptor = TableDescriptorBuilder.newBuilder(tableName).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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 = this.tableName;
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 testHonorsMergeRequestMaxNumberOfRegionsCount() {
502    conf.setBoolean(SPLIT_ENABLED_KEY, false);
503    conf.setInt(MERGE_MIN_REGION_COUNT_KEY, 1);
504    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
505    conf.setInt(MERGE_REQUEST_MAX_NUMBER_OF_REGIONS_COUNT_KEY, 3);
506    final TableName tableName = this.tableName;
507    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 5);
508    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 0, 1, 0, 1, 0);
509    setupMocksForNormalizer(regionSizes, regionInfos);
510    assertEquals(3, normalizer.getMergeRequestMaxNumberOfRegionsCount());
511    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
512    assertThat(plans,
513      contains(
514        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
515          .addTarget(regionInfos.get(1), 1).addTarget(regionInfos.get(2), 0).build(),
516        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(3), 1)
517          .addTarget(regionInfos.get(4), 0).build()));
518  }
519
520  @Test
521  public void testHonorsMergeRequestMaxNumberOfRegionsCountDefault() {
522    conf.setBoolean(SPLIT_ENABLED_KEY, false);
523    conf.setInt(MERGE_MIN_REGION_COUNT_KEY, 1);
524    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
525    final TableName tableName = this.tableName;
526    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 3);
527    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 0, 0, 0);
528    setupMocksForNormalizer(regionSizes, regionInfos);
529    assertEquals(100, normalizer.getMergeRequestMaxNumberOfRegionsCount());
530    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
531    assertThat(plans, contains(new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
532      .addTarget(regionInfos.get(1), 0).addTarget(regionInfos.get(2), 0).build()));
533  }
534
535  @Test
536  public void testMergeEmptyRegions0() {
537    conf.setBoolean(SPLIT_ENABLED_KEY, false);
538    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
539    final TableName tableName = this.tableName;
540    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 7);
541    final Map<byte[], Integer> regionSizes =
542      createRegionSizesMap(regionInfos, 0, 1, 10, 0, 9, 10, 0);
543    setupMocksForNormalizer(regionSizes, regionInfos);
544
545    assertFalse(normalizer.isSplitEnabled());
546    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
547    assertThat(normalizer.computePlansForTable(tableDescriptor),
548      contains(
549        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
550          .addTarget(regionInfos.get(1), 1).build(),
551        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(2), 10)
552          .addTarget(regionInfos.get(3), 0).build(),
553        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(5), 10)
554          .addTarget(regionInfos.get(6), 0).build()));
555  }
556
557  @Test
558  public void testMergeEmptyRegions1() {
559    conf.setBoolean(SPLIT_ENABLED_KEY, false);
560    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
561    final TableName tableName = this.tableName;
562    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 8);
563    final Map<byte[], Integer> regionSizes =
564      createRegionSizesMap(regionInfos, 0, 1, 10, 0, 9, 0, 10, 0);
565    setupMocksForNormalizer(regionSizes, regionInfos);
566
567    assertFalse(normalizer.isSplitEnabled());
568    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
569    assertThat(normalizer.computePlansForTable(tableDescriptor),
570      contains(
571        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
572          .addTarget(regionInfos.get(1), 1).build(),
573        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(2), 10)
574          .addTarget(regionInfos.get(3), 0).build(),
575        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(4), 9)
576          .addTarget(regionInfos.get(5), 0).build(),
577        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(6), 10)
578          .addTarget(regionInfos.get(7), 0).build()));
579  }
580
581  @Test
582  public void testMergeEmptyRegions2() {
583    conf.setBoolean(SPLIT_ENABLED_KEY, false);
584    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
585    final TableName tableName = this.tableName;
586    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 8);
587    final Map<byte[], Integer> regionSizes =
588      createRegionSizesMap(regionInfos, 0, 10, 1, 0, 9, 0, 10, 0);
589    setupMocksForNormalizer(regionSizes, regionInfos);
590
591    assertFalse(normalizer.isSplitEnabled());
592    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
593    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
594    assertThat(plans,
595      contains(
596        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 0)
597          .addTarget(regionInfos.get(1), 10).build(),
598        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(2), 1)
599          .addTarget(regionInfos.get(3), 0).build(),
600        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(4), 9)
601          .addTarget(regionInfos.get(5), 0).build(),
602        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(6), 10)
603          .addTarget(regionInfos.get(7), 0).build()));
604  }
605
606  @Test
607  public void testSplitAndMultiMerge() {
608    conf.setInt(MERGE_MIN_REGION_SIZE_MB_KEY, 0);
609    final TableName tableName = this.tableName;
610    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 8);
611    final Map<byte[], Integer> regionSizes =
612      createRegionSizesMap(regionInfos, 3, 1, 1, 30, 9, 3, 1, 0);
613    setupMocksForNormalizer(regionSizes, regionInfos);
614
615    assertTrue(normalizer.isMergeEnabled());
616    assertTrue(normalizer.isSplitEnabled());
617    assertEquals(0, normalizer.getMergeMinRegionSizeMb());
618    assertThat(normalizer.computePlansForTable(tableDescriptor),
619      contains(new SplitNormalizationPlan(regionInfos.get(3), 30),
620        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(0), 3)
621          .addTarget(regionInfos.get(1), 1).addTarget(regionInfos.get(2), 1).build(),
622        new MergeNormalizationPlan.Builder().addTarget(regionInfos.get(5), 3)
623          .addTarget(regionInfos.get(6), 1).addTarget(regionInfos.get(7), 0).build()));
624  }
625
626  // This test is to make sure that normalizer is only going to merge adjacent regions.
627  @Test
628  public void testNormalizerCannotMergeNonAdjacentRegions() {
629    final TableName tableName = this.tableName;
630    // create 5 regions with sizes to trigger merge of small regions. region ranges are:
631    // [, "aa"), ["aa", "aa1"), ["aa1", "aa1!"), ["aa1!", "aa2"), ["aa2", )
632    // Region ["aa", "aa1") and ["aa1!", "aa2") are not adjacent, they are not supposed to
633    // merged.
634    final byte[][] keys = { null, Bytes.toBytes("aa"), Bytes.toBytes("aa1!"), Bytes.toBytes("aa1"),
635      Bytes.toBytes("aa2"), null, };
636    final List<RegionInfo> regionInfos = createRegionInfos(tableName, keys);
637    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 3, 1, 1, 3, 5);
638    setupMocksForNormalizer(regionSizes, regionInfos);
639
640    // Compute the plan, no merge plan returned as they are not adjacent.
641    List<NormalizationPlan> plans = normalizer.computePlansForTable(tableDescriptor);
642    assertThat(plans, empty());
643  }
644
645  @Test
646  public void testSizeLimitShufflesPlans() {
647    conf.setLong(CUMULATIVE_SIZE_LIMIT_MB_KEY, 10);
648    final TableName tableName = this.tableName;
649    final List<RegionInfo> regionInfos = createRegionInfos(tableName, 4);
650    final Map<byte[], Integer> regionSizes = createRegionSizesMap(regionInfos, 3, 3, 3, 3);
651    setupMocksForNormalizer(regionSizes, regionInfos);
652    when(tableDescriptor.getNormalizerTargetRegionSize()).thenReturn(1L);
653    normalizer = spy(normalizer);
654
655    assertTrue(normalizer.isSplitEnabled());
656    assertTrue(normalizer.isMergeEnabled());
657    List<NormalizationPlan> computedPlans = normalizer.computePlansForTable(tableDescriptor);
658    assertThat(computedPlans, hasSize(4));
659    verify(normalizer, times(1)).shuffleNormalizationPlans(anyList());
660  }
661
662  @SuppressWarnings("MockitoCast")
663  private void setupMocksForNormalizer(Map<byte[], Integer> regionSizes,
664    List<RegionInfo> regionInfoList) {
665    masterServices = Mockito.mock(MasterServices.class, RETURNS_DEEP_STUBS);
666    tableDescriptor = Mockito.mock(TableDescriptor.class, RETURNS_DEEP_STUBS);
667
668    // for simplicity all regions are assumed to be on one server; doesn't matter to us
669    ServerName sn = ServerName.valueOf("localhost", 0, 0L);
670    when(masterServices.getAssignmentManager().getRegionStates().getRegionsOfTable(any()))
671      .thenReturn(regionInfoList);
672    when(masterServices.getAssignmentManager().getRegionStates().getRegionServerOfRegion(any()))
673      .thenReturn(sn);
674    when(
675      masterServices.getAssignmentManager().getRegionStates().getRegionState(any(RegionInfo.class)))
676      .thenReturn(RegionState.createForTesting(null, RegionState.State.OPEN));
677
678    for (Map.Entry<byte[], Integer> region : regionSizes.entrySet()) {
679      RegionMetrics regionLoad = Mockito.mock(RegionMetrics.class);
680      when(regionLoad.getRegionName()).thenReturn(region.getKey());
681      when(regionLoad.getStoreFileSize())
682        .thenReturn(new Size(region.getValue(), Size.Unit.MEGABYTE));
683
684      // this is possibly broken with jdk9, unclear if false positive or not
685      // suppress it for now, fix it when we get to running tests on 9
686      // see: http://errorprone.info/bugpattern/MockitoCast
687      when((Object) masterServices.getServerManager().getLoad(sn).getRegionMetrics()
688        .get(region.getKey())).thenReturn(regionLoad);
689    }
690
691    when(masterServices.isSplitOrMergeEnabled(any())).thenReturn(true);
692    when(tableDescriptor.getTableName()).thenReturn(tableName);
693
694    normalizer = new SimpleRegionNormalizer();
695    normalizer.setConf(conf);
696    normalizer.setMasterServices(masterServices);
697  }
698
699  /**
700   * Create a list of {@link RegionInfo}s that represent a region chain of the specified length.
701   */
702  private static List<RegionInfo> createRegionInfos(final TableName tableName, final int length) {
703    if (length < 1) {
704      throw new IllegalStateException("length must be greater than or equal to 1.");
705    }
706
707    final byte[] startKey = Bytes.toBytes("aaaaa");
708    final byte[] endKey = Bytes.toBytes("zzzzz");
709    if (length == 1) {
710      return Collections.singletonList(createRegionInfo(tableName, startKey, endKey));
711    }
712
713    final byte[][] splitKeys = Bytes.split(startKey, endKey, length - 1);
714    final List<RegionInfo> ret = new ArrayList<>(length);
715    for (int i = 0; i < splitKeys.length - 1; i++) {
716      ret.add(createRegionInfo(tableName, splitKeys[i], splitKeys[i + 1]));
717    }
718    return ret;
719  }
720
721  private static RegionInfo createRegionInfo(final TableName tableName, final byte[] startKey,
722    final byte[] endKey) {
723    return RegionInfoBuilder.newBuilder(tableName).setStartKey(startKey).setEndKey(endKey)
724      .setRegionId(generateRegionId()).build();
725  }
726
727  private static long generateRegionId() {
728    return Instant.ofEpochMilli(EnvironmentEdgeManager.currentTime())
729      .minus(Period.ofDays(DEFAULT_MERGE_MIN_REGION_AGE_DAYS + 1)).toEpochMilli();
730  }
731
732  private static List<RegionInfo> createRegionInfos(final TableName tableName,
733    final byte[][] splitKeys) {
734    final List<RegionInfo> ret = new ArrayList<>(splitKeys.length);
735    for (int i = 0; i < splitKeys.length - 1; i++) {
736      ret.add(createRegionInfo(tableName, splitKeys[i], splitKeys[i + 1]));
737    }
738    return ret;
739  }
740
741  private static Map<byte[], Integer> createRegionSizesMap(final List<RegionInfo> regionInfos,
742    int... sizes) {
743    if (regionInfos.size() != sizes.length) {
744      throw new IllegalStateException("Parameter lengths must match.");
745    }
746
747    final Map<byte[], Integer> ret = new HashMap<>(regionInfos.size());
748    for (int i = 0; i < regionInfos.size(); i++) {
749      ret.put(regionInfos.get(i).getRegionName(), sizes[i]);
750    }
751    return ret;
752  }
753}