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