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.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils.isEmpty;
021
022import java.time.Instant;
023import java.time.Period;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Objects;
029import java.util.function.BooleanSupplier;
030import java.util.function.Function;
031import org.apache.hadoop.conf.Configuration;
032import org.apache.hadoop.hbase.HBaseInterfaceAudience;
033import org.apache.hadoop.hbase.RegionMetrics;
034import org.apache.hadoop.hbase.ServerMetrics;
035import org.apache.hadoop.hbase.ServerName;
036import org.apache.hadoop.hbase.Size;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.MasterSwitchType;
039import org.apache.hadoop.hbase.client.RegionInfo;
040import org.apache.hadoop.hbase.client.TableDescriptor;
041import org.apache.hadoop.hbase.conf.ConfigurationObserver;
042import org.apache.hadoop.hbase.master.MasterServices;
043import org.apache.hadoop.hbase.master.RegionState;
044import org.apache.hadoop.hbase.master.assignment.RegionStates;
045import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
046import org.apache.yetus.audience.InterfaceAudience;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * Simple implementation of region normalizer. Logic in use:
052 * <ol>
053 * <li>Get all regions of a given table</li>
054 * <li>Get avg size S of the regions in the table (by total size of store files reported in
055 * RegionMetrics)</li>
056 * <li>For each region R0, if R0 is bigger than S * 2, it is kindly requested to split.</li>
057 * <li>Otherwise, for the next region in the chain R1, if R0 + R1 is smaller then S, R0 and R1 are
058 * kindly requested to merge.</li>
059 * </ol>
060 */
061@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
062class SimpleRegionNormalizer implements RegionNormalizer, ConfigurationObserver {
063  private static final Logger LOG = LoggerFactory.getLogger(SimpleRegionNormalizer.class);
064
065  static final String SPLIT_ENABLED_KEY = "hbase.normalizer.split.enabled";
066  static final boolean DEFAULT_SPLIT_ENABLED = true;
067  static final String MERGE_ENABLED_KEY = "hbase.normalizer.merge.enabled";
068  static final boolean DEFAULT_MERGE_ENABLED = true;
069  /**
070   * @deprecated since 2.5.0 and will be removed in 4.0.0. Use
071   *             {@link SimpleRegionNormalizer#MERGE_MIN_REGION_COUNT_KEY} instead.
072   * @see <a href="https://issues.apache.org/jira/browse/HBASE-25745">HBASE-25745</a>
073   */
074  @Deprecated
075  static final String MIN_REGION_COUNT_KEY = "hbase.normalizer.min.region.count";
076  static final String MERGE_MIN_REGION_COUNT_KEY = "hbase.normalizer.merge.min.region.count";
077  static final int DEFAULT_MERGE_MIN_REGION_COUNT = 3;
078  static final String MERGE_MIN_REGION_AGE_DAYS_KEY = "hbase.normalizer.merge.min_region_age.days";
079  static final int DEFAULT_MERGE_MIN_REGION_AGE_DAYS = 3;
080  static final String MERGE_MIN_REGION_SIZE_MB_KEY = "hbase.normalizer.merge.min_region_size.mb";
081  static final int DEFAULT_MERGE_MIN_REGION_SIZE_MB = 0;
082
083  private MasterServices masterServices;
084  private NormalizerConfiguration normalizerConfiguration;
085
086  public SimpleRegionNormalizer() {
087    masterServices = null;
088    normalizerConfiguration = new NormalizerConfiguration();
089  }
090
091  @Override
092  public Configuration getConf() {
093    return normalizerConfiguration.getConf();
094  }
095
096  @Override
097  public void setConf(final Configuration conf) {
098    if (conf == null) {
099      return;
100    }
101    normalizerConfiguration = new NormalizerConfiguration(conf, normalizerConfiguration);
102  }
103
104  @Override
105  public void onConfigurationChange(Configuration conf) {
106    LOG.debug("Updating configuration parameters according to new configuration instance.");
107    setConf(conf);
108  }
109
110  private static int parseMergeMinRegionCount(final Configuration conf) {
111    final int parsedValue = conf.getInt(MERGE_MIN_REGION_COUNT_KEY, DEFAULT_MERGE_MIN_REGION_COUNT);
112    final int settledValue = Math.max(1, parsedValue);
113    if (parsedValue != settledValue) {
114      warnInvalidValue(MERGE_MIN_REGION_COUNT_KEY, parsedValue, settledValue);
115    }
116    return settledValue;
117  }
118
119  private static Period parseMergeMinRegionAge(final Configuration conf) {
120    final int parsedValue =
121      conf.getInt(MERGE_MIN_REGION_AGE_DAYS_KEY, DEFAULT_MERGE_MIN_REGION_AGE_DAYS);
122    final int settledValue = Math.max(0, parsedValue);
123    if (parsedValue != settledValue) {
124      warnInvalidValue(MERGE_MIN_REGION_AGE_DAYS_KEY, parsedValue, settledValue);
125    }
126    return Period.ofDays(settledValue);
127  }
128
129  private static long parseMergeMinRegionSizeMb(final Configuration conf) {
130    final long parsedValue =
131      conf.getLong(MERGE_MIN_REGION_SIZE_MB_KEY, DEFAULT_MERGE_MIN_REGION_SIZE_MB);
132    final long settledValue = Math.max(0, parsedValue);
133    if (parsedValue != settledValue) {
134      warnInvalidValue(MERGE_MIN_REGION_SIZE_MB_KEY, parsedValue, settledValue);
135    }
136    return settledValue;
137  }
138
139  private static <T> void warnInvalidValue(final String key, final T parsedValue,
140    final T settledValue) {
141    LOG.warn("Configured value {}={} is invalid. Setting value to {}.", key, parsedValue,
142      settledValue);
143  }
144
145  private static <T> void logConfigurationUpdated(final String key, final T oldValue,
146    final T newValue) {
147    if (!Objects.equals(oldValue, newValue)) {
148      LOG.info("Updated configuration for key '{}' from {} to {}", key, oldValue, newValue);
149    }
150  }
151
152  /**
153   * Return this instance's configured value for {@value #SPLIT_ENABLED_KEY}.
154   */
155  public boolean isSplitEnabled() {
156    return normalizerConfiguration.isSplitEnabled();
157  }
158
159  /**
160   * Return this instance's configured value for {@value #MERGE_ENABLED_KEY}.
161   */
162  public boolean isMergeEnabled() {
163    return normalizerConfiguration.isMergeEnabled();
164  }
165
166  /**
167   * Return this instance's configured value for {@value #MERGE_MIN_REGION_COUNT_KEY}.
168   */
169  public int getMergeMinRegionCount() {
170    return normalizerConfiguration.getMergeMinRegionCount();
171  }
172
173  /**
174   * Return this instance's configured value for {@value #MERGE_MIN_REGION_AGE_DAYS_KEY}.
175   */
176  public Period getMergeMinRegionAge() {
177    return normalizerConfiguration.getMergeMinRegionAge();
178  }
179
180  /**
181   * Return this instance's configured value for {@value #MERGE_MIN_REGION_SIZE_MB_KEY}.
182   */
183  public long getMergeMinRegionSizeMb() {
184    return normalizerConfiguration.getMergeMinRegionSizeMb();
185  }
186
187  @Override
188  public void setMasterServices(final MasterServices masterServices) {
189    this.masterServices = masterServices;
190  }
191
192  @Override
193  public List<NormalizationPlan> computePlansForTable(final TableDescriptor tableDescriptor) {
194    if (tableDescriptor == null) {
195      return Collections.emptyList();
196    }
197    TableName table = tableDescriptor.getTableName();
198    if (table.isSystemTable()) {
199      LOG.debug("Normalization of system table {} isn't allowed", table);
200      return Collections.emptyList();
201    }
202
203    final boolean proceedWithSplitPlanning = proceedWithSplitPlanning(tableDescriptor);
204    final boolean proceedWithMergePlanning = proceedWithMergePlanning(tableDescriptor);
205    if (!proceedWithMergePlanning && !proceedWithSplitPlanning) {
206      LOG.debug("Both split and merge are disabled. Skipping normalization of table: {}", table);
207      return Collections.emptyList();
208    }
209
210    final NormalizeContext ctx = new NormalizeContext(tableDescriptor);
211    if (isEmpty(ctx.getTableRegions())) {
212      return Collections.emptyList();
213    }
214
215    LOG.debug("Computing normalization plan for table:  {}, number of regions: {}", table,
216      ctx.getTableRegions().size());
217
218    final List<NormalizationPlan> plans = new ArrayList<>();
219    int splitPlansCount = 0;
220    if (proceedWithSplitPlanning) {
221      List<NormalizationPlan> splitPlans = computeSplitNormalizationPlans(ctx);
222      splitPlansCount = splitPlans.size();
223      plans.addAll(splitPlans);
224    }
225    int mergePlansCount = 0;
226    if (proceedWithMergePlanning) {
227      List<NormalizationPlan> mergePlans = computeMergeNormalizationPlans(ctx);
228      mergePlansCount = mergePlans.size();
229      plans.addAll(mergePlans);
230    }
231
232    LOG.debug("Computed normalization plans for table {}. Total plans: {}, split plans: {}, "
233      + "merge plans: {}", table, plans.size(), splitPlansCount, mergePlansCount);
234    return plans;
235  }
236
237  /** Returns size of region in MB and if region is not found than -1 */
238  private long getRegionSizeMB(RegionInfo hri) {
239    ServerName sn =
240      masterServices.getAssignmentManager().getRegionStates().getRegionServerOfRegion(hri);
241    if (sn == null) {
242      LOG.debug("{} region was not found on any Server", hri.getRegionNameAsString());
243      return -1;
244    }
245    ServerMetrics serverMetrics = masterServices.getServerManager().getLoad(sn);
246    if (serverMetrics == null) {
247      LOG.debug("server {} was not found in ServerManager", sn.getServerName());
248      return -1;
249    }
250    RegionMetrics regionLoad = serverMetrics.getRegionMetrics().get(hri.getRegionName());
251    if (regionLoad == null) {
252      LOG.debug("{} was not found in RegionsLoad", hri.getRegionNameAsString());
253      return -1;
254    }
255    return (long) regionLoad.getStoreFileSize().get(Size.Unit.MEGABYTE);
256  }
257
258  private boolean isMasterSwitchEnabled(final MasterSwitchType masterSwitchType) {
259    return masterServices.isSplitOrMergeEnabled(masterSwitchType);
260  }
261
262  private boolean proceedWithSplitPlanning(TableDescriptor tableDescriptor) {
263    String value = tableDescriptor.getValue(SPLIT_ENABLED_KEY);
264    return (value == null ? isSplitEnabled() : Boolean.parseBoolean(value))
265      && isMasterSwitchEnabled(MasterSwitchType.SPLIT);
266  }
267
268  private boolean proceedWithMergePlanning(TableDescriptor tableDescriptor) {
269    String value = tableDescriptor.getValue(MERGE_ENABLED_KEY);
270    return (value == null ? isMergeEnabled() : Boolean.parseBoolean(value))
271      && isMasterSwitchEnabled(MasterSwitchType.MERGE);
272  }
273
274  /**
275   * Also make sure tableRegions contains regions of the same table
276   * @param tableRegions    regions of table to normalize
277   * @param tableDescriptor the TableDescriptor
278   * @return average region size depending on
279   * @see TableDescriptor#getNormalizerTargetRegionCount()
280   */
281  private double getAverageRegionSizeMb(final List<RegionInfo> tableRegions,
282    final TableDescriptor tableDescriptor) {
283    if (isEmpty(tableRegions)) {
284      throw new IllegalStateException(
285        "Cannot calculate average size of a table without any regions.");
286    }
287    TableName table = tableDescriptor.getTableName();
288    double avgRegionSize;
289    int targetRegionCount = tableDescriptor.getNormalizerTargetRegionCount();
290    long targetRegionSize = tableDescriptor.getNormalizerTargetRegionSize();
291    LOG.debug("Table {} configured with target region count {}, target region size {} MB", table,
292      targetRegionCount, targetRegionSize);
293
294    if (targetRegionSize > 0) {
295      avgRegionSize = targetRegionSize;
296    } else {
297      final int regionCount = tableRegions.size();
298      final long totalSizeMb = tableRegions.stream().mapToLong(this::getRegionSizeMB).sum();
299      if (targetRegionCount > 0) {
300        avgRegionSize = totalSizeMb / (double) targetRegionCount;
301      } else {
302        avgRegionSize = totalSizeMb / (double) regionCount;
303      }
304      LOG.debug("Table {}, total aggregated regions size: {} MB and average region size {} MB",
305        table, totalSizeMb, String.format("%.3f", avgRegionSize));
306    }
307
308    return avgRegionSize;
309  }
310
311  /**
312   * Determine if a {@link RegionInfo} should be considered for a merge operation.
313   * </p>
314   * Callers beware: for safe concurrency, be sure to pass in the local instance of
315   * {@link NormalizerConfiguration}, don't use {@code this}'s instance.
316   */
317  private boolean skipForMerge(final NormalizerConfiguration normalizerConfiguration,
318    final NormalizeContext ctx, final RegionInfo regionInfo) {
319    final RegionState state = ctx.getRegionStates().getRegionState(regionInfo);
320    final String name = regionInfo.getEncodedName();
321    return logTraceReason(() -> state == null,
322      "skipping merge of region {} because no state information is available.", name)
323      || logTraceReason(() -> !Objects.equals(state.getState(), RegionState.State.OPEN),
324        "skipping merge of region {} because it is not open.", name)
325      || logTraceReason(() -> !isOldEnoughForMerge(normalizerConfiguration, ctx, regionInfo),
326        "skipping merge of region {} because it is not old enough.", name)
327      || logTraceReason(() -> !isLargeEnoughForMerge(normalizerConfiguration, ctx, regionInfo),
328        "skipping merge region {} because it is not large enough.", name);
329  }
330
331  /**
332   * Computes the merge plans that should be executed for this table to converge average region
333   * towards target average or target region count.
334   */
335  private List<NormalizationPlan> computeMergeNormalizationPlans(final NormalizeContext ctx) {
336    final NormalizerConfiguration configuration = normalizerConfiguration;
337    if (ctx.getTableRegions().size() < configuration.getMergeMinRegionCount(ctx)) {
338      LOG.debug(
339        "Table {} has {} regions, required min number of regions for normalizer to run"
340          + " is {}, not computing merge plans.",
341        ctx.getTableName(), ctx.getTableRegions().size(), configuration.getMergeMinRegionCount());
342      return Collections.emptyList();
343    }
344
345    final long avgRegionSizeMb = (long) ctx.getAverageRegionSizeMb();
346    if (avgRegionSizeMb < configuration.getMergeMinRegionSizeMb(ctx)) {
347      return Collections.emptyList();
348    }
349    LOG.debug("Computing normalization plan for table {}. average region size: {} MB, number of"
350      + " regions: {}.", ctx.getTableName(), avgRegionSizeMb, ctx.getTableRegions().size());
351
352    // this nested loop walks the table's region chain once, looking for contiguous sequences of
353    // regions that meet the criteria for merge. The outer loop tracks the starting point of the
354    // next sequence, the inner loop looks for the end of that sequence. A single sequence becomes
355    // an instance of MergeNormalizationPlan.
356
357    final List<NormalizationPlan> plans = new LinkedList<>();
358    final List<NormalizationTarget> rangeMembers = new LinkedList<>();
359    long sumRangeMembersSizeMb;
360    int current = 0;
361    for (int rangeStart = 0; rangeStart < ctx.getTableRegions().size() - 1
362      && current < ctx.getTableRegions().size();) {
363      // walk the region chain looking for contiguous sequences of regions that can be merged.
364      rangeMembers.clear();
365      sumRangeMembersSizeMb = 0;
366      for (current = rangeStart; current < ctx.getTableRegions().size(); current++) {
367        final RegionInfo regionInfo = ctx.getTableRegions().get(current);
368        final long regionSizeMb = getRegionSizeMB(regionInfo);
369        if (skipForMerge(configuration, ctx, regionInfo)) {
370          // this region cannot participate in a range. resume the outer loop.
371          rangeStart = Math.max(current, rangeStart + 1);
372          break;
373        }
374        if (
375          rangeMembers.isEmpty() // when there are no range members, seed the range with whatever
376                                 // we have. this way we're prepared in case the next region is
377                                 // 0-size.
378            || (rangeMembers.size() == 1 && sumRangeMembersSizeMb == 0) // when there is only one
379                                                                        // region and the size is 0,
380                                                                        // seed the range with
381                                                                        // whatever we have.
382            || regionSizeMb == 0 // always add an empty region to the current range.
383            || (regionSizeMb + sumRangeMembersSizeMb <= avgRegionSizeMb)
384        ) { // add the current region
385            // to the range when
386            // there's capacity
387            // remaining.
388          rangeMembers.add(new NormalizationTarget(regionInfo, regionSizeMb));
389          sumRangeMembersSizeMb += regionSizeMb;
390          continue;
391        }
392        // we have accumulated enough regions to fill a range. resume the outer loop.
393        rangeStart = Math.max(current, rangeStart + 1);
394        break;
395      }
396      if (rangeMembers.size() > 1) {
397        plans.add(new MergeNormalizationPlan.Builder().setTargets(rangeMembers).build());
398      }
399    }
400    return plans;
401  }
402
403  /**
404   * Determine if a region in {@link RegionState} should be considered for a split operation.
405   */
406  private static boolean skipForSplit(final RegionState state, final RegionInfo regionInfo) {
407    final String name = regionInfo.getEncodedName();
408    return logTraceReason(() -> state == null,
409      "skipping split of region {} because no state information is available.", name)
410      || logTraceReason(() -> !Objects.equals(state.getState(), RegionState.State.OPEN),
411        "skipping merge of region {} because it is not open.", name);
412  }
413
414  /**
415   * Computes the split plans that should be executed for this table to converge average region size
416   * towards target average or target region count. <br />
417   * if the region is > 2 times larger than average, we split it. split is more high priority
418   * normalization action than merge.
419   */
420  private List<NormalizationPlan> computeSplitNormalizationPlans(final NormalizeContext ctx) {
421    final double avgRegionSize = ctx.getAverageRegionSizeMb();
422    LOG.debug("Table {}, average region size: {} MB", ctx.getTableName(),
423      String.format("%.3f", avgRegionSize));
424
425    final List<NormalizationPlan> plans = new ArrayList<>();
426    for (final RegionInfo hri : ctx.getTableRegions()) {
427      if (skipForSplit(ctx.getRegionStates().getRegionState(hri), hri)) {
428        continue;
429      }
430      final long regionSizeMb = getRegionSizeMB(hri);
431      if (regionSizeMb > 2 * avgRegionSize) {
432        LOG.info(
433          "Table {}, large region {} has size {} MB, more than twice avg size {} MB, "
434            + "splitting",
435          ctx.getTableName(), hri.getRegionNameAsString(), regionSizeMb,
436          String.format("%.3f", avgRegionSize));
437        plans.add(new SplitNormalizationPlan(hri, regionSizeMb));
438      }
439    }
440    return plans;
441  }
442
443  /**
444   * Return {@code true} when {@code regionInfo} has a creation date that is old enough to be
445   * considered for a merge operation, {@code false} otherwise.
446   */
447  private static boolean isOldEnoughForMerge(final NormalizerConfiguration normalizerConfiguration,
448    final NormalizeContext ctx, final RegionInfo regionInfo) {
449    final Instant currentTime = Instant.ofEpochMilli(EnvironmentEdgeManager.currentTime());
450    final Instant regionCreateTime = Instant.ofEpochMilli(regionInfo.getRegionId());
451    return currentTime
452      .isAfter(regionCreateTime.plus(normalizerConfiguration.getMergeMinRegionAge(ctx)));
453  }
454
455  /**
456   * Return {@code true} when {@code regionInfo} has a size that is sufficient to be considered for
457   * a merge operation, {@code false} otherwise.
458   * </p>
459   * Callers beware: for safe concurrency, be sure to pass in the local instance of
460   * {@link NormalizerConfiguration}, don't use {@code this}'s instance.
461   */
462  private boolean isLargeEnoughForMerge(final NormalizerConfiguration normalizerConfiguration,
463    final NormalizeContext ctx, final RegionInfo regionInfo) {
464    return getRegionSizeMB(regionInfo) >= normalizerConfiguration.getMergeMinRegionSizeMb(ctx);
465  }
466
467  private static boolean logTraceReason(final BooleanSupplier predicate, final String fmtWhenTrue,
468    final Object... args) {
469    final boolean value = predicate.getAsBoolean();
470    if (value) {
471      LOG.trace(fmtWhenTrue, args);
472    }
473    return value;
474  }
475
476  /**
477   * Holds the configuration values read from {@link Configuration}. Encapsulation in a POJO enables
478   * atomic hot-reloading of configs without locks.
479   */
480  private static final class NormalizerConfiguration {
481    private final Configuration conf;
482    private final boolean splitEnabled;
483    private final boolean mergeEnabled;
484    private final int mergeMinRegionCount;
485    private final Period mergeMinRegionAge;
486    private final long mergeMinRegionSizeMb;
487
488    private NormalizerConfiguration() {
489      conf = null;
490      splitEnabled = DEFAULT_SPLIT_ENABLED;
491      mergeEnabled = DEFAULT_MERGE_ENABLED;
492      mergeMinRegionCount = DEFAULT_MERGE_MIN_REGION_COUNT;
493      mergeMinRegionAge = Period.ofDays(DEFAULT_MERGE_MIN_REGION_AGE_DAYS);
494      mergeMinRegionSizeMb = DEFAULT_MERGE_MIN_REGION_SIZE_MB;
495    }
496
497    private NormalizerConfiguration(final Configuration conf,
498      final NormalizerConfiguration currentConfiguration) {
499      this.conf = conf;
500      splitEnabled = conf.getBoolean(SPLIT_ENABLED_KEY, DEFAULT_SPLIT_ENABLED);
501      mergeEnabled = conf.getBoolean(MERGE_ENABLED_KEY, DEFAULT_MERGE_ENABLED);
502      mergeMinRegionCount = parseMergeMinRegionCount(conf);
503      mergeMinRegionAge = parseMergeMinRegionAge(conf);
504      mergeMinRegionSizeMb = parseMergeMinRegionSizeMb(conf);
505      logConfigurationUpdated(SPLIT_ENABLED_KEY, currentConfiguration.isSplitEnabled(),
506        splitEnabled);
507      logConfigurationUpdated(MERGE_ENABLED_KEY, currentConfiguration.isMergeEnabled(),
508        mergeEnabled);
509      logConfigurationUpdated(MERGE_MIN_REGION_COUNT_KEY,
510        currentConfiguration.getMergeMinRegionCount(), mergeMinRegionCount);
511      logConfigurationUpdated(MERGE_MIN_REGION_AGE_DAYS_KEY,
512        currentConfiguration.getMergeMinRegionAge(), mergeMinRegionAge);
513      logConfigurationUpdated(MERGE_MIN_REGION_SIZE_MB_KEY,
514        currentConfiguration.getMergeMinRegionSizeMb(), mergeMinRegionSizeMb);
515    }
516
517    public Configuration getConf() {
518      return conf;
519    }
520
521    public boolean isSplitEnabled() {
522      return splitEnabled;
523    }
524
525    public boolean isMergeEnabled() {
526      return mergeEnabled;
527    }
528
529    public int getMergeMinRegionCount() {
530      return mergeMinRegionCount;
531    }
532
533    public int getMergeMinRegionCount(NormalizeContext context) {
534      String stringValue =
535        context.getOrDefault(MERGE_MIN_REGION_COUNT_KEY, Function.identity(), null);
536      if (stringValue == null) {
537        stringValue = context.getOrDefault(MIN_REGION_COUNT_KEY, Function.identity(), null);
538        if (stringValue != null) {
539          LOG.debug(
540            "The config key {} in table descriptor is deprecated. Instead please use {}. "
541              + "In future release we will remove the deprecated config.",
542            MIN_REGION_COUNT_KEY, MERGE_MIN_REGION_COUNT_KEY);
543        }
544      }
545      final int mergeMinRegionCount = stringValue == null ? 0 : Integer.parseInt(stringValue);
546      if (mergeMinRegionCount <= 0) {
547        return getMergeMinRegionCount();
548      }
549      return mergeMinRegionCount;
550    }
551
552    public Period getMergeMinRegionAge() {
553      return mergeMinRegionAge;
554    }
555
556    public Period getMergeMinRegionAge(NormalizeContext context) {
557      final int mergeMinRegionAge =
558        context.getOrDefault(MERGE_MIN_REGION_AGE_DAYS_KEY, Integer::parseInt, -1);
559      if (mergeMinRegionAge < 0) {
560        return getMergeMinRegionAge();
561      }
562      return Period.ofDays(mergeMinRegionAge);
563    }
564
565    public long getMergeMinRegionSizeMb() {
566      return mergeMinRegionSizeMb;
567    }
568
569    public long getMergeMinRegionSizeMb(NormalizeContext context) {
570      final long mergeMinRegionSizeMb =
571        context.getOrDefault(MERGE_MIN_REGION_SIZE_MB_KEY, Long::parseLong, (long) -1);
572      if (mergeMinRegionSizeMb < 0) {
573        return getMergeMinRegionSizeMb();
574      }
575      return mergeMinRegionSizeMb;
576    }
577  }
578
579  /**
580   * Inner class caries the state necessary to perform a single invocation of
581   * {@link #computePlansForTable(TableDescriptor)}. Grabbing this data from the assignment manager
582   * up-front allows any computed values to be realized just once.
583   */
584  private class NormalizeContext {
585    private final TableName tableName;
586    private final RegionStates regionStates;
587    private final List<RegionInfo> tableRegions;
588    private final double averageRegionSizeMb;
589    private final TableDescriptor tableDescriptor;
590
591    public NormalizeContext(final TableDescriptor tableDescriptor) {
592      this.tableDescriptor = tableDescriptor;
593      tableName = tableDescriptor.getTableName();
594      regionStates =
595        SimpleRegionNormalizer.this.masterServices.getAssignmentManager().getRegionStates();
596      tableRegions = regionStates.getRegionsOfTable(tableName);
597      // The list of regionInfo from getRegionsOfTable() is ordered by regionName.
598      // regionName does not necessary guarantee the order by STARTKEY (let's say 'aa1', 'aa1!',
599      // in order by regionName, it will be 'aa1!' followed by 'aa1').
600      // This could result in normalizer merging non-adjacent regions into one and creates overlaps.
601      // In order to avoid that, sort the list by RegionInfo.COMPARATOR.
602      // See HBASE-24376
603      tableRegions.sort(RegionInfo.COMPARATOR);
604      averageRegionSizeMb =
605        SimpleRegionNormalizer.this.getAverageRegionSizeMb(this.tableRegions, this.tableDescriptor);
606    }
607
608    public TableName getTableName() {
609      return tableName;
610    }
611
612    public RegionStates getRegionStates() {
613      return regionStates;
614    }
615
616    public List<RegionInfo> getTableRegions() {
617      return tableRegions;
618    }
619
620    public double getAverageRegionSizeMb() {
621      return averageRegionSizeMb;
622    }
623
624    public <T> T getOrDefault(String key, Function<String, T> function, T defaultValue) {
625      String value = tableDescriptor.getValue(key);
626      if (value == null) {
627        return defaultValue;
628      } else {
629        return function.apply(value);
630      }
631    }
632  }
633}