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 org.hamcrest.Matchers.comparesEqualTo;
021import static org.hamcrest.Matchers.greaterThanOrEqualTo;
022import static org.hamcrest.Matchers.lessThanOrEqualTo;
023import static org.hamcrest.Matchers.not;
024import static org.junit.jupiter.api.Assertions.assertEquals;
025import static org.junit.jupiter.api.Assertions.assertFalse;
026import static org.junit.jupiter.api.Assertions.assertNotNull;
027import static org.junit.jupiter.api.Assertions.assertTrue;
028
029import java.io.IOException;
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.Comparator;
033import java.util.List;
034import java.util.concurrent.TimeUnit;
035import org.apache.hadoop.hbase.HBaseTestingUtil;
036import org.apache.hadoop.hbase.HConstants;
037import org.apache.hadoop.hbase.MatcherPredicate;
038import org.apache.hadoop.hbase.NamespaceDescriptor;
039import org.apache.hadoop.hbase.RegionMetrics;
040import org.apache.hadoop.hbase.ServerName;
041import org.apache.hadoop.hbase.Size;
042import org.apache.hadoop.hbase.TableName;
043import org.apache.hadoop.hbase.Waiter.ExplainingPredicate;
044import org.apache.hadoop.hbase.client.AsyncAdmin;
045import org.apache.hadoop.hbase.client.NormalizeTableFilterParams;
046import org.apache.hadoop.hbase.client.Put;
047import org.apache.hadoop.hbase.client.RegionInfo;
048import org.apache.hadoop.hbase.client.RegionLocator;
049import org.apache.hadoop.hbase.client.Table;
050import org.apache.hadoop.hbase.client.TableDescriptor;
051import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
052import org.apache.hadoop.hbase.master.HMaster;
053import org.apache.hadoop.hbase.master.MasterServices;
054import org.apache.hadoop.hbase.master.TableNamespaceManager;
055import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan.PlanType;
056import org.apache.hadoop.hbase.namespace.TestNamespaceAuditor;
057import org.apache.hadoop.hbase.quotas.QuotaUtil;
058import org.apache.hadoop.hbase.regionserver.HRegion;
059import org.apache.hadoop.hbase.regionserver.Region;
060import org.apache.hadoop.hbase.testclassification.LargeTests;
061import org.apache.hadoop.hbase.testclassification.MasterTests;
062import org.apache.hadoop.hbase.util.Bytes;
063import org.apache.hadoop.hbase.util.LoadTestKVGenerator;
064import org.hamcrest.Matcher;
065import org.hamcrest.Matchers;
066import org.junit.jupiter.api.AfterAll;
067import org.junit.jupiter.api.BeforeAll;
068import org.junit.jupiter.api.BeforeEach;
069import org.junit.jupiter.api.Tag;
070import org.junit.jupiter.api.Test;
071import org.junit.jupiter.api.TestInfo;
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074
075/**
076 * Testing {@link SimpleRegionNormalizer} on minicluster.
077 */
078@Tag(MasterTests.TAG)
079@Tag(LargeTests.TAG)
080public class TestSimpleRegionNormalizerOnCluster {
081  private static final Logger LOG =
082    LoggerFactory.getLogger(TestSimpleRegionNormalizerOnCluster.class);
083
084  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
085  private static final byte[] FAMILY_NAME = Bytes.toBytes("fam");
086
087  private static AsyncAdmin admin;
088  private static HMaster master;
089
090  @BeforeAll
091  public static void beforeAllTests() throws Exception {
092    // we will retry operations when PleaseHoldException is thrown
093    TEST_UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 3);
094    TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true);
095
096    // no way for the test to set the regionId on a created region, so disable this feature.
097    TEST_UTIL.getConfiguration().setInt("hbase.normalizer.merge.min_region_age.days", 0);
098
099    // disable the normalizer coming along and running via Chore
100    TEST_UTIL.getConfiguration().setInt("hbase.normalizer.period", Integer.MAX_VALUE);
101
102    TEST_UTIL.startMiniCluster(1);
103    TestNamespaceAuditor.waitForQuotaInitialize(TEST_UTIL);
104    admin = TEST_UTIL.getAsyncConnection().getAdmin();
105    master = TEST_UTIL.getHBaseCluster().getMaster();
106    assertNotNull(master);
107  }
108
109  @AfterAll
110  public static void afterAllTests() throws Exception {
111    TEST_UTIL.shutdownMiniCluster();
112  }
113
114  @BeforeEach
115  public void before() throws Exception {
116    // disable the normalizer ahead of time, let the test enable it when its ready.
117    admin.normalizerSwitch(false).get();
118  }
119
120  @Test
121  public void testHonorsNormalizerSwitch() throws Exception {
122    assertFalse(admin.isNormalizerEnabled().get());
123    assertFalse(admin.normalize().get());
124    assertFalse(admin.normalizerSwitch(true).get());
125    assertTrue(admin.normalize().get());
126  }
127
128  /**
129   * Test that disabling normalizer via table configuration is honored. There's no side-effect to
130   * look for (other than a log message), so normalize two tables, one with the disabled setting,
131   * and look for change in one and no change in the other.
132   */
133  @Test
134  public void testHonorsNormalizerTableSetting(TestInfo testInfo) throws Exception {
135    final TableName tn1 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "1");
136    final TableName tn2 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "2");
137    final TableName tn3 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "3");
138
139    try {
140      final int tn1RegionCount = createTableBegsSplit(tn1, true, false);
141      final int tn2RegionCount = createTableBegsSplit(tn2, false, false);
142      final int tn3RegionCount = createTableBegsSplit(tn3, true, true);
143
144      assertFalse(admin.normalizerSwitch(true).get());
145      assertTrue(admin.normalize().get());
146      waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1));
147
148      // confirm that tn1 has (tn1RegionCount + 1) number of regions.
149      // tn2 has tn2RegionCount number of regions because normalizer has not been enabled on it.
150      // tn3 has tn3RegionCount number of regions because two plans are run:
151      // 1. split one region to two
152      // 2. merge two regions into one
153      // and hence, total number of regions for tn3 remains same
154      assertEquals(tn1RegionCount + 1, getRegionCount(tn1), tn1 + " should have split.");
155      assertEquals(tn2RegionCount, getRegionCount(tn2), tn2 + " should not have split.");
156      LOG.debug("waiting for t3 to settle...");
157      waitForTableRegionCount(tn3, comparesEqualTo(tn3RegionCount));
158    } finally {
159      dropIfExists(tn1);
160      dropIfExists(tn2);
161      dropIfExists(tn3);
162    }
163  }
164
165  @Test
166  public void testRegionNormalizationSplitWithoutQuotaLimit(TestInfo testInfo) throws Exception {
167    testRegionNormalizationSplit(false, testInfo);
168  }
169
170  @Test
171  public void testRegionNormalizationSplitWithQuotaLimit(TestInfo testInfo) throws Exception {
172    testRegionNormalizationSplit(true, testInfo);
173  }
174
175  void testRegionNormalizationSplit(boolean limitedByQuota, TestInfo testInfo) throws Exception {
176    TableName tableName = null;
177    try {
178      tableName = limitedByQuota
179        ? buildTableNameForQuotaTest(testInfo.getTestMethod().get().getName())
180        : TableName.valueOf(testInfo.getTestMethod().get().getName());
181
182      final int currentRegionCount = createTableBegsSplit(tableName, true, false);
183      final long existingSkippedSplitCount =
184        master.getRegionNormalizerManager().getSkippedCount(PlanType.SPLIT);
185      assertFalse(admin.normalizerSwitch(true).get());
186      assertTrue(admin.normalize().get());
187      if (limitedByQuota) {
188        waitForSkippedSplits(master, existingSkippedSplitCount);
189        assertEquals(currentRegionCount, getRegionCount(tableName),
190          tableName + " should not have split.");
191      } else {
192        waitForTableRegionCount(tableName, greaterThanOrEqualTo(currentRegionCount + 1));
193        assertEquals(currentRegionCount + 1, getRegionCount(tableName),
194          tableName + " should have split.");
195      }
196    } finally {
197      dropIfExists(tableName);
198    }
199  }
200
201  @Test
202  public void testRegionNormalizationMerge(TestInfo testInfo) throws Exception {
203    final TableName tableName = TableName.valueOf(testInfo.getTestMethod().get().getName());
204    try {
205      final int currentRegionCount = createTableBegsMerge(tableName);
206      assertFalse(admin.normalizerSwitch(true).get());
207      assertTrue(admin.normalize().get());
208      waitForTableRegionCount(tableName, lessThanOrEqualTo(currentRegionCount - 1));
209      assertEquals(currentRegionCount - 1, getRegionCount(tableName),
210        tableName + " should have merged.");
211    } finally {
212      dropIfExists(tableName);
213    }
214  }
215
216  @Test
217  public void testHonorsNamespaceFilter(TestInfo testInfo) throws Exception {
218    final NamespaceDescriptor namespaceDescriptor = NamespaceDescriptor.create("ns").build();
219    final TableName tn1 = TableName.valueOf("ns", testInfo.getTestMethod().get().getName());
220    final TableName tn2 = TableName.valueOf(testInfo.getTestMethod().get().getName());
221
222    try {
223      admin.createNamespace(namespaceDescriptor).get();
224      final int tn1RegionCount = createTableBegsSplit(tn1, true, false);
225      final int tn2RegionCount = createTableBegsSplit(tn2, true, false);
226      final NormalizeTableFilterParams ntfp =
227        new NormalizeTableFilterParams.Builder().namespace("ns").build();
228
229      assertFalse(admin.normalizerSwitch(true).get());
230      assertTrue(admin.normalize(ntfp).get());
231      waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1));
232
233      // confirm that tn1 has (tn1RegionCount + 1) number of regions.
234      // tn2 has tn2RegionCount number of regions because it's not a member of the target namespace.
235      assertEquals(tn1RegionCount + 1, getRegionCount(tn1), tn1 + " should have split.");
236      waitForTableRegionCount(tn2, comparesEqualTo(tn2RegionCount));
237    } finally {
238      dropIfExists(tn1);
239      dropIfExists(tn2);
240    }
241  }
242
243  @Test
244  public void testHonorsPatternFilter(TestInfo testInfo) throws Exception {
245    final TableName tn1 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "1");
246    final TableName tn2 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "2");
247
248    try {
249      final int tn1RegionCount = createTableBegsSplit(tn1, true, false);
250      final int tn2RegionCount = createTableBegsSplit(tn2, true, false);
251      final NormalizeTableFilterParams ntfp =
252        new NormalizeTableFilterParams.Builder().regex(".*[1]").build();
253
254      assertFalse(admin.normalizerSwitch(true).get());
255      assertTrue(admin.normalize(ntfp).get());
256      waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1));
257
258      // confirm that tn1 has (tn1RegionCount + 1) number of regions.
259      // tn2 has tn2RegionCount number of regions because it fails filter.
260      assertEquals(tn1RegionCount + 1, getRegionCount(tn1), tn1 + " should have split.");
261      waitForTableRegionCount(tn2, comparesEqualTo(tn2RegionCount));
262    } finally {
263      dropIfExists(tn1);
264      dropIfExists(tn2);
265    }
266  }
267
268  @Test
269  public void testHonorsNameFilter(TestInfo testInfo) throws Exception {
270    final TableName tn1 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "1");
271    final TableName tn2 = TableName.valueOf(testInfo.getTestMethod().get().getName() + "2");
272
273    try {
274      final int tn1RegionCount = createTableBegsSplit(tn1, true, false);
275      final int tn2RegionCount = createTableBegsSplit(tn2, true, false);
276      final NormalizeTableFilterParams ntfp =
277        new NormalizeTableFilterParams.Builder().tableNames(Collections.singletonList(tn1)).build();
278
279      assertFalse(admin.normalizerSwitch(true).get());
280      assertTrue(admin.normalize(ntfp).get());
281      waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1));
282
283      // confirm that tn1 has (tn1RegionCount + 1) number of regions.
284      // tn2 has tn3RegionCount number of regions because it fails filter:
285      assertEquals(tn1RegionCount + 1, getRegionCount(tn1), tn1 + " should have split.");
286      waitForTableRegionCount(tn2, comparesEqualTo(tn2RegionCount));
287    } finally {
288      dropIfExists(tn1);
289      dropIfExists(tn2);
290    }
291  }
292
293  /**
294   * A test for when a region is the target of both a split and a merge plan. Does not define
295   * expected behavior, only that some change is applied to the table.
296   */
297  @Test
298  public void testTargetOfSplitAndMerge(TestInfo testInfo) throws Exception {
299    final TableName tn = TableName.valueOf(testInfo.getTestMethod().get().getName());
300    try {
301      final int tnRegionCount = createTableTargetOfSplitAndMerge(tn);
302      assertFalse(admin.normalizerSwitch(true).get());
303      assertTrue(admin.normalize().get());
304      TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5),
305        new MatcherPredicate<>("expected " + tn + " to split or merge (probably split)",
306          () -> getRegionCountUnchecked(tn), not(comparesEqualTo(tnRegionCount))));
307    } finally {
308      dropIfExists(tn);
309    }
310  }
311
312  private static TableName buildTableNameForQuotaTest(final String methodName) throws Exception {
313    String nsp = "np2";
314    NamespaceDescriptor nspDesc =
315      NamespaceDescriptor.create(nsp).addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "5")
316        .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build();
317    admin.createNamespace(nspDesc).get();
318    return TableName.valueOf(nsp, methodName);
319  }
320
321  private static void waitForSkippedSplits(final HMaster master,
322    final long existingSkippedSplitCount) {
323    TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5),
324      new MatcherPredicate<>("waiting to observe split attempt and skipped.",
325        () -> master.getRegionNormalizerManager().getSkippedCount(PlanType.SPLIT),
326        Matchers.greaterThan(existingSkippedSplitCount)));
327  }
328
329  private static void waitForTableRegionCount(final TableName tableName,
330    Matcher<? super Integer> matcher) {
331    TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5),
332      new MatcherPredicate<>("region count for table " + tableName + " does not match expected",
333        () -> getRegionCountUnchecked(tableName), matcher));
334  }
335
336  private static List<HRegion> generateTestData(final TableName tableName,
337    final int... regionSizesMb) throws IOException {
338    final List<HRegion> generatedRegions;
339    final int numRegions = regionSizesMb.length;
340    LOG.debug("generating test data into {}, {} regions of sizes (mb) {}", tableName, numRegions,
341      regionSizesMb);
342    try (Table ignored = TEST_UTIL.createMultiRegionTable(tableName, FAMILY_NAME, numRegions)) {
343      // Need to get sorted list of regions here
344      generatedRegions = new ArrayList<>(TEST_UTIL.getHBaseCluster().getRegions(tableName));
345      generatedRegions.sort(Comparator.comparing(HRegion::getRegionInfo, RegionInfo.COMPARATOR));
346      assertEquals(numRegions, generatedRegions.size());
347      for (int i = 0; i < numRegions; i++) {
348        HRegion region = generatedRegions.get(i);
349        generateTestData(region, regionSizesMb[i]);
350        region.flush(true);
351      }
352    }
353    return generatedRegions;
354  }
355
356  private static void generateTestData(Region region, int numRows) throws IOException {
357    // generating 1Mb values
358    LOG.debug("writing {}mb to {}", numRows, region);
359    LoadTestKVGenerator dataGenerator = new LoadTestKVGenerator(1024 * 1024, 1024 * 1024);
360    for (int i = 0; i < numRows; ++i) {
361      byte[] key = Bytes.add(region.getRegionInfo().getStartKey(), Bytes.toBytes(i));
362      for (int j = 0; j < 1; ++j) {
363        Put put = new Put(key);
364        byte[] col = Bytes.toBytes(String.valueOf(j));
365        byte[] value = dataGenerator.generateRandomSizeValue(key, col);
366        put.addColumn(FAMILY_NAME, col, value);
367        region.put(put);
368      }
369    }
370  }
371
372  private static double getRegionSizeMB(final MasterServices masterServices,
373    final RegionInfo regionInfo) {
374    final ServerName sn =
375      masterServices.getAssignmentManager().getRegionStates().getRegionServerOfRegion(regionInfo);
376    final RegionMetrics regionLoad = masterServices.getServerManager().getLoad(sn)
377      .getRegionMetrics().get(regionInfo.getRegionName());
378    if (regionLoad == null) {
379      LOG.debug("{} was not found in RegionsLoad", regionInfo.getRegionNameAsString());
380      return -1;
381    }
382    return regionLoad.getStoreFileSize().get(Size.Unit.MEGABYTE);
383  }
384
385  /**
386   * create a table with 5 regions, having region sizes so as to provoke a split of the largest
387   * region.
388   * <ul>
389   * <li>total table size: 12</li>
390   * <li>average region size: 2.4</li>
391   * <li>split threshold: 2.4 * 2 = 4.8</li>
392   * </ul>
393   */
394  private static int createTableBegsSplit(final TableName tableName,
395    final boolean normalizerEnabled, final boolean isMergeEnabled) throws Exception {
396    final List<HRegion> generatedRegions = generateTestData(tableName, 1, 1, 2, 3, 5);
397    assertEquals(5, getRegionCount(tableName));
398    admin.flush(tableName).get();
399
400    final TableDescriptor td =
401      TableDescriptorBuilder.newBuilder(admin.getDescriptor(tableName).get())
402        .setNormalizationEnabled(normalizerEnabled).setMergeEnabled(isMergeEnabled).build();
403    admin.modifyTable(td).get();
404
405    // make sure relatively accurate region statistics are available for the test table. use
406    // the last/largest region as clue.
407    TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(1), new ExplainingPredicate<IOException>() {
408      @Override
409      public String explainFailure() {
410        return "expected largest region to be >= 4mb.";
411      }
412
413      @Override
414      public boolean evaluate() {
415        return generatedRegions.stream()
416          .mapToDouble(val -> getRegionSizeMB(master, val.getRegionInfo())).allMatch(val -> val > 0)
417          && getRegionSizeMB(master, generatedRegions.get(4).getRegionInfo()) >= 4.0;
418      }
419    });
420    return 5;
421  }
422
423  /**
424   * create a table with 5 regions, having region sizes so as to provoke a merge of the smallest
425   * regions.
426   * <ul>
427   * <li>total table size: 13</li>
428   * <li>average region size: 2.6</li>
429   * <li>sum of sizes of first two regions < average</li>
430   * </ul>
431   */
432  private static int createTableBegsMerge(final TableName tableName) throws Exception {
433    // create 5 regions with sizes to trigger merge of small regions
434    final List<HRegion> generatedRegions = generateTestData(tableName, 1, 1, 3, 3, 5);
435    assertEquals(5, getRegionCount(tableName));
436    admin.flush(tableName).get();
437
438    final TableDescriptor td = TableDescriptorBuilder
439      .newBuilder(admin.getDescriptor(tableName).get()).setNormalizationEnabled(true).build();
440    admin.modifyTable(td).get();
441
442    // make sure relatively accurate region statistics are available for the test table. use
443    // the last/largest region as clue.
444    LOG.debug("waiting for region statistics to settle.");
445    TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(1), new ExplainingPredicate<IOException>() {
446      @Override
447      public String explainFailure() {
448        return "expected largest region to be >= 4mb.";
449      }
450
451      @Override
452      public boolean evaluate() {
453        return generatedRegions.stream()
454          .mapToDouble(val -> getRegionSizeMB(master, val.getRegionInfo())).allMatch(val -> val > 0)
455          && getRegionSizeMB(master, generatedRegions.get(4).getRegionInfo()) >= 4.0;
456      }
457    });
458    return 5;
459  }
460
461  /**
462   * Create a table with 4 regions, having region sizes so as to provoke a split of the largest
463   * region and a merge of an empty region into the largest.
464   * <ul>
465   * <li>total table size: 14</li>
466   * <li>average region size: 3.5</li>
467   * </ul>
468   */
469  private static int createTableTargetOfSplitAndMerge(final TableName tableName) throws Exception {
470    final int[] regionSizesMb = { 10, 0, 2, 2 };
471    final List<HRegion> generatedRegions = generateTestData(tableName, regionSizesMb);
472    assertEquals(4, getRegionCount(tableName));
473    admin.flush(tableName).get();
474
475    final TableDescriptor td = TableDescriptorBuilder
476      .newBuilder(admin.getDescriptor(tableName).get()).setNormalizationEnabled(true).build();
477    admin.modifyTable(td).get();
478
479    // make sure relatively accurate region statistics are available for the test table. use
480    // the last/largest region as clue.
481    LOG.debug("waiting for region statistics to settle.");
482    TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5), new ExplainingPredicate<IOException>() {
483      @Override
484      public String explainFailure() {
485        return "expected largest region to be >= 10mb.";
486      }
487
488      @Override
489      public boolean evaluate() {
490        for (int i = 0; i < generatedRegions.size(); i++) {
491          final RegionInfo regionInfo = generatedRegions.get(i).getRegionInfo();
492          if (!(getRegionSizeMB(master, regionInfo) >= regionSizesMb[i])) {
493            return false;
494          }
495        }
496        return true;
497      }
498    });
499    return 4;
500  }
501
502  private static void dropIfExists(final TableName tableName) throws Exception {
503    if (tableName != null && admin.tableExists(tableName).get()) {
504      if (admin.isTableEnabled(tableName).get()) {
505        admin.disableTable(tableName).get();
506      }
507      admin.deleteTable(tableName).get();
508    }
509  }
510
511  private static int getRegionCount(TableName tableName) throws IOException {
512    try (RegionLocator locator = TEST_UTIL.getConnection().getRegionLocator(tableName)) {
513      return locator.getAllRegionLocations().size();
514    }
515  }
516
517  private static int getRegionCountUnchecked(final TableName tableName) {
518    try {
519      return getRegionCount(tableName);
520    } catch (IOException e) {
521      throw new RuntimeException(e);
522    }
523  }
524}