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.assignment;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.List;
026import java.util.stream.Stream;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.fs.FileSystem;
029import org.apache.hadoop.fs.Path;
030import org.apache.hadoop.hbase.MetaMutationAnnotation;
031import org.apache.hadoop.hbase.ServerName;
032import org.apache.hadoop.hbase.TableName;
033import org.apache.hadoop.hbase.UnknownRegionException;
034import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
035import org.apache.hadoop.hbase.client.DoNotRetryRegionException;
036import org.apache.hadoop.hbase.client.MasterSwitchType;
037import org.apache.hadoop.hbase.client.Mutation;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.client.RegionInfoBuilder;
040import org.apache.hadoop.hbase.client.TableDescriptor;
041import org.apache.hadoop.hbase.exceptions.MergeRegionException;
042import org.apache.hadoop.hbase.io.hfile.CacheConfig;
043import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
044import org.apache.hadoop.hbase.master.MasterFileSystem;
045import org.apache.hadoop.hbase.master.RegionState;
046import org.apache.hadoop.hbase.master.RegionState.State;
047import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan;
048import org.apache.hadoop.hbase.master.procedure.AbstractStateMachineTableProcedure;
049import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
050import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil;
051import org.apache.hadoop.hbase.procedure2.ProcedureMetrics;
052import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
053import org.apache.hadoop.hbase.quotas.MasterQuotaManager;
054import org.apache.hadoop.hbase.quotas.QuotaExceededException;
055import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
056import org.apache.hadoop.hbase.regionserver.HStoreFile;
057import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
058import org.apache.hadoop.hbase.regionserver.StoreUtils;
059import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker;
060import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory;
061import org.apache.hadoop.hbase.util.Bytes;
062import org.apache.hadoop.hbase.util.CommonFSUtils;
063import org.apache.hadoop.hbase.wal.WALSplitUtil;
064import org.apache.yetus.audience.InterfaceAudience;
065import org.slf4j.Logger;
066import org.slf4j.LoggerFactory;
067
068import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
069import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoResponse;
070import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
071import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.MergeTableRegionsState;
072
073/**
074 * The procedure to Merge regions in a table. This procedure takes an exclusive table lock since it
075 * is working over multiple regions. It holds the lock for the life of the procedure. Throws
076 * exception on construction if determines context hostile to merge (cluster going down or master is
077 * shutting down or table is disabled).
078 */
079@InterfaceAudience.Private
080public class MergeTableRegionsProcedure
081  extends AbstractStateMachineTableProcedure<MergeTableRegionsState> {
082  private static final Logger LOG = LoggerFactory.getLogger(MergeTableRegionsProcedure.class);
083  private ServerName regionLocation;
084
085  /**
086   * Two or more regions to merge, the 'merge parents'.
087   */
088  private RegionInfo[] regionsToMerge;
089
090  /**
091   * The resulting merged region.
092   */
093  private RegionInfo mergedRegion;
094
095  private boolean force;
096
097  public MergeTableRegionsProcedure() {
098    // Required by the Procedure framework to create the procedure on replay
099  }
100
101  public MergeTableRegionsProcedure(final MasterProcedureEnv env, final RegionInfo[] regionsToMerge,
102    final boolean force) throws IOException {
103    super(env);
104    // Check parent regions. Make sure valid before starting work.
105    // This check calls the super method #checkOnline also.
106    checkRegionsToMerge(env, regionsToMerge, force);
107    // Sort the regions going into the merge.
108    Arrays.sort(regionsToMerge);
109    this.regionsToMerge = regionsToMerge;
110    this.mergedRegion = createMergedRegionInfo(regionsToMerge);
111    // Preflight depends on mergedRegion being set (at least).
112    preflightChecks(env, true);
113    this.force = force;
114  }
115
116  /**
117   * @throws MergeRegionException If unable to merge regions for whatever reasons.
118   */
119  private static void checkRegionsToMerge(MasterProcedureEnv env, final RegionInfo[] regions,
120    final boolean force) throws MergeRegionException {
121    long count = Arrays.stream(regions).distinct().count();
122    if (regions.length != count) {
123      throw new MergeRegionException("Duplicate regions specified; cannot merge a region to "
124        + "itself. Passed in " + regions.length + " but only " + count + " unique.");
125    }
126    if (count < 2) {
127      throw new MergeRegionException("Need two Regions at least to run a Merge");
128    }
129    RegionInfo previous = null;
130    for (RegionInfo ri : regions) {
131      if (previous != null) {
132        if (!previous.getTable().equals(ri.getTable())) {
133          String msg = "Can't merge regions from different tables: " + previous + ", " + ri;
134          LOG.warn(msg);
135          throw new MergeRegionException(msg);
136        }
137        if (!force && !ri.isAdjacent(previous) && !ri.isOverlap(previous)) {
138          String msg = "Unable to merge non-adjacent or non-overlapping regions '"
139            + previous.getShortNameToLog() + "', '" + ri.getShortNameToLog() + "' when force=false";
140          LOG.warn(msg);
141          throw new MergeRegionException(msg);
142        }
143      }
144
145      if (ri.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
146        throw new MergeRegionException("Can't merge non-default replicas; " + ri);
147      }
148      try {
149        checkOnline(env, ri);
150      } catch (DoNotRetryRegionException dnrre) {
151        throw new MergeRegionException(dnrre);
152      }
153
154      previous = ri;
155    }
156  }
157
158  /**
159   * Create merged region info by looking at passed in <code>regionsToMerge</code> to figure what
160   * extremes for start and end keys to use; merged region needs to have an extent sufficient to
161   * cover all regions-to-merge.
162   */
163  private static RegionInfo createMergedRegionInfo(final RegionInfo[] regionsToMerge) {
164    byte[] lowestStartKey = null;
165    byte[] highestEndKey = null;
166    // Region Id is a timestamp. Merged region's id can't be less than that of
167    // merging regions else will insert at wrong location in hbase:meta (See HBASE-710).
168    long highestRegionId = -1;
169    for (RegionInfo ri : regionsToMerge) {
170      if (lowestStartKey == null) {
171        lowestStartKey = ri.getStartKey();
172      } else if (Bytes.compareTo(ri.getStartKey(), lowestStartKey) < 0) {
173        lowestStartKey = ri.getStartKey();
174      }
175      if (highestEndKey == null) {
176        highestEndKey = ri.getEndKey();
177      } else if (ri.isLast() || Bytes.compareTo(ri.getEndKey(), highestEndKey) > 0) {
178        highestEndKey = ri.getEndKey();
179      }
180      highestRegionId = ri.getRegionId() > highestRegionId ? ri.getRegionId() : highestRegionId;
181    }
182    // Merged region is sorted between two merging regions in META
183    return RegionInfoBuilder.newBuilder(regionsToMerge[0].getTable()).setStartKey(lowestStartKey)
184      .setEndKey(highestEndKey).setSplit(false)
185      .setRegionId(highestRegionId + 1/* Add one so new merged region is highest */).build();
186  }
187
188  @Override
189  protected Flow executeFromState(final MasterProcedureEnv env, MergeTableRegionsState state) {
190    LOG.trace("{} execute state={}", this, state);
191    try {
192      switch (state) {
193        case MERGE_TABLE_REGIONS_PREPARE:
194          if (!prepareMergeRegion(env)) {
195            assert isFailed() : "Merge region should have an exception here";
196            return Flow.NO_MORE_STATE;
197          }
198          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION);
199          break;
200        case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION:
201          preMergeRegions(env);
202          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CLOSE_REGIONS);
203          break;
204        case MERGE_TABLE_REGIONS_CLOSE_REGIONS:
205          addChildProcedure(createUnassignProcedures(env));
206          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS);
207          break;
208        case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
209          checkClosedRegions(env);
210          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CREATE_MERGED_REGION);
211          break;
212        case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION:
213          removeNonDefaultReplicas(env);
214          createMergedRegion(env);
215          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE);
216          break;
217        case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:
218          writeMaxSequenceIdFile(env);
219          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION);
220          break;
221        case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION:
222          preMergeRegionsCommit(env);
223          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_UPDATE_META);
224          break;
225        case MERGE_TABLE_REGIONS_UPDATE_META:
226          updateMetaForMergedRegions(env);
227          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION);
228          break;
229        case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
230          postMergeRegionsCommit(env);
231          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_OPEN_MERGED_REGION);
232          break;
233        case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
234          addChildProcedure(createAssignProcedures(env));
235          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_OPERATION);
236          break;
237        case MERGE_TABLE_REGIONS_POST_OPERATION:
238          postCompletedMergeRegions(env);
239          return Flow.NO_MORE_STATE;
240        default:
241          throw new UnsupportedOperationException(this + " unhandled state=" + state);
242      }
243    } catch (IOException e) {
244      String msg = "Error trying to merge " + RegionInfo.getShortNameToLog(regionsToMerge) + " in "
245        + getTableName() + " (in state=" + state + ")";
246      if (!isRollbackSupported(state)) {
247        // We reach a state that cannot be rolled back. We just need to keep retrying.
248        LOG.warn(msg, e);
249      } else {
250        LOG.error(msg, e);
251        setFailure("master-merge-regions", e);
252      }
253    }
254    return Flow.HAS_MORE_STATE;
255  }
256
257  /**
258   * To rollback {@link MergeTableRegionsProcedure}, two AssignProcedures are asynchronously
259   * submitted for each region to be merged (rollback doesn't wait on the completion of the
260   * AssignProcedures) . This can be improved by changing rollback() to support sub-procedures. See
261   * HBASE-19851 for details.
262   */
263  @Override
264  protected void rollbackState(final MasterProcedureEnv env, final MergeTableRegionsState state)
265    throws IOException {
266    LOG.trace("{} rollback state={}", this, state);
267
268    try {
269      switch (state) {
270        case MERGE_TABLE_REGIONS_POST_OPERATION:
271        case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
272        case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
273        case MERGE_TABLE_REGIONS_UPDATE_META:
274          String msg = this + " We are in the " + state + " state."
275            + " It is complicated to rollback the merge operation that region server is working on."
276            + " Rollback is not supported and we should let the merge operation to complete";
277          LOG.warn(msg);
278          // PONR
279          throw new UnsupportedOperationException(this + " unhandled state=" + state);
280        case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION:
281          break;
282        case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION:
283        case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:
284          cleanupMergedRegion(env);
285          break;
286        case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
287          rollbackCloseRegionsForMerge(env);
288          break;
289        case MERGE_TABLE_REGIONS_CLOSE_REGIONS:
290          // No-op. The reopen TransitRegionStateProcedures for the two parents were already
291          // submitted by the rollback of MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS above;
292          // submitting a second batch here would race the first. Mirrors
293          // SplitTableRegionProcedure's rollback of SPLIT_TABLE_REGION_CLOSE_PARENT_REGION.
294          break;
295        case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION:
296          postRollBackMergeRegions(env);
297          break;
298        case MERGE_TABLE_REGIONS_PREPARE:
299          rollbackPrepareMerge(env);
300          break;
301        default:
302          throw new UnsupportedOperationException(this + " unhandled state=" + state);
303      }
304    } catch (Exception e) {
305      // This will be retried. Unless there is a bug in the code,
306      // this should be just a "temporary error" (e.g. network down)
307      LOG.warn("Failed rollback attempt step " + state + " for merging the regions "
308        + RegionInfo.getShortNameToLog(regionsToMerge) + " in table " + getTableName(), e);
309      throw e;
310    }
311  }
312
313  /*
314   * Check whether we are in the state that can be rolled back
315   */
316  @Override
317  protected boolean isRollbackSupported(final MergeTableRegionsState state) {
318    switch (state) {
319      case MERGE_TABLE_REGIONS_POST_OPERATION:
320      case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
321      case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
322      case MERGE_TABLE_REGIONS_UPDATE_META:
323        // It is not safe to rollback in these states.
324        return false;
325      default:
326        break;
327    }
328    return true;
329  }
330
331  private void removeNonDefaultReplicas(MasterProcedureEnv env) throws IOException {
332    AssignmentManagerUtil.removeNonDefaultReplicas(env, Stream.of(regionsToMerge),
333      getRegionReplication(env));
334  }
335
336  private void checkClosedRegions(MasterProcedureEnv env) throws IOException {
337    // Theoretically this should not happen any more after we use TRSP, but anyway
338    // let's add a check here
339    for (RegionInfo region : regionsToMerge) {
340      AssignmentManagerUtil.checkClosedRegion(env, region);
341    }
342  }
343
344  @Override
345  protected MergeTableRegionsState getState(final int stateId) {
346    return MergeTableRegionsState.forNumber(stateId);
347  }
348
349  @Override
350  protected int getStateId(final MergeTableRegionsState state) {
351    return state.getNumber();
352  }
353
354  @Override
355  protected MergeTableRegionsState getInitialState() {
356    return MergeTableRegionsState.MERGE_TABLE_REGIONS_PREPARE;
357  }
358
359  @Override
360  protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException {
361    super.serializeStateData(serializer);
362
363    final MasterProcedureProtos.MergeTableRegionsStateData.Builder mergeTableRegionsMsg =
364      MasterProcedureProtos.MergeTableRegionsStateData.newBuilder()
365        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
366        .setMergedRegionInfo(ProtobufUtil.toRegionInfo(mergedRegion)).setForcible(force);
367    for (RegionInfo ri : regionsToMerge) {
368      mergeTableRegionsMsg.addRegionInfo(ProtobufUtil.toRegionInfo(ri));
369    }
370    serializer.serialize(mergeTableRegionsMsg.build());
371  }
372
373  @Override
374  protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException {
375    super.deserializeStateData(serializer);
376
377    final MasterProcedureProtos.MergeTableRegionsStateData mergeTableRegionsMsg =
378      serializer.deserialize(MasterProcedureProtos.MergeTableRegionsStateData.class);
379    setUser(MasterProcedureUtil.toUserInfo(mergeTableRegionsMsg.getUserInfo()));
380
381    assert (mergeTableRegionsMsg.getRegionInfoCount() == 2);
382    regionsToMerge = new RegionInfo[mergeTableRegionsMsg.getRegionInfoCount()];
383    for (int i = 0; i < regionsToMerge.length; i++) {
384      regionsToMerge[i] = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getRegionInfo(i));
385    }
386
387    mergedRegion = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getMergedRegionInfo());
388  }
389
390  @Override
391  public void toStringClassDetails(StringBuilder sb) {
392    sb.append(getClass().getSimpleName());
393    sb.append(" table=");
394    sb.append(getTableName());
395    sb.append(", regions=");
396    sb.append(RegionInfo.getShortNameToLog(regionsToMerge));
397    sb.append(", force=");
398    sb.append(force);
399  }
400
401  @Override
402  protected LockState acquireLock(final MasterProcedureEnv env) {
403    RegionInfo[] lockRegions = Arrays.copyOf(regionsToMerge, regionsToMerge.length + 1);
404    lockRegions[lockRegions.length - 1] = mergedRegion;
405
406    if (env.getProcedureScheduler().waitRegions(this, getTableName(), lockRegions)) {
407      try {
408        LOG.debug(LockState.LOCK_EVENT_WAIT + " " + env.getProcedureScheduler().dumpLocks());
409      } catch (IOException e) {
410        // Ignore, just for logging
411      }
412      return LockState.LOCK_EVENT_WAIT;
413    }
414    return LockState.LOCK_ACQUIRED;
415  }
416
417  @Override
418  protected void releaseLock(final MasterProcedureEnv env) {
419    RegionInfo[] lockRegions = Arrays.copyOf(regionsToMerge, regionsToMerge.length + 1);
420    lockRegions[lockRegions.length - 1] = mergedRegion;
421
422    env.getProcedureScheduler().wakeRegions(this, getTableName(), lockRegions);
423  }
424
425  @Override
426  protected boolean holdLock(MasterProcedureEnv env) {
427    return true;
428  }
429
430  @Override
431  public TableName getTableName() {
432    return mergedRegion.getTable();
433  }
434
435  @Override
436  public TableOperationType getTableOperationType() {
437    return TableOperationType.REGION_MERGE;
438  }
439
440  @Override
441  protected ProcedureMetrics getProcedureMetrics(MasterProcedureEnv env) {
442    return env.getAssignmentManager().getAssignmentManagerMetrics().getMergeProcMetrics();
443  }
444
445  /**
446   * Prepare merge and do some check
447   */
448  private boolean prepareMergeRegion(final MasterProcedureEnv env) throws IOException {
449    // Fail if we are taking snapshot for the given table
450    TableName tn = regionsToMerge[0].getTable();
451    final String regionNamesToLog = RegionInfo.getShortNameToLog(regionsToMerge);
452    if (env.getMasterServices().getSnapshotManager().isTableTakingAnySnapshot(tn)) {
453      throw new MergeRegionException(
454        "Skip merging regions " + regionNamesToLog + ", because we are snapshotting " + tn);
455    }
456
457    /*
458     * Sometimes a ModifyTableProcedure has edited a table descriptor to change the number of region
459     * replicas for a table, but it has not yet opened/closed the new replicas. The
460     * ModifyTableProcedure assumes that nobody else will do the opening/closing of the new
461     * replicas, but a concurrent MergeTableRegionProcedure would violate that assumption.
462     */
463    if (isTableModificationInProgress(env)) {
464      setFailure(getClass().getSimpleName(),
465        new IOException("Skip merging regions " + regionNamesToLog
466          + ", because there is an active procedure that is modifying the table " + tn));
467      return false;
468    }
469
470    // Mostly the below two checks are not used because we already check the switches before
471    // submitting the merge procedure. Just for safety, we are checking the switch again here.
472    // Also, in case the switch was set to false after submission, this procedure can be rollbacked,
473    // thanks to this double check!
474    // case 1: check for cluster level switch
475    if (!env.getMasterServices().isSplitOrMergeEnabled(MasterSwitchType.MERGE)) {
476      LOG.warn("Merge switch is off! skip merge of " + regionNamesToLog);
477      setFailure(getClass().getSimpleName(),
478        new IOException("Merge of " + regionNamesToLog + " failed because merge switch is off"));
479      return false;
480    }
481    // case 2: check for table level switch
482    if (!env.getMasterServices().getTableDescriptors().get(getTableName()).isMergeEnabled()) {
483      LOG.warn("Merge is disabled for the table! Skipping merge of {}", regionNamesToLog);
484      setFailure(getClass().getSimpleName(), new IOException(
485        "Merge of " + regionNamesToLog + " failed as region merge is disabled for the table"));
486      return false;
487    }
488
489    RegionStates regionStates = env.getAssignmentManager().getRegionStates();
490    RegionStateStore regionStateStore = env.getAssignmentManager().getRegionStateStore();
491    for (RegionInfo ri : this.regionsToMerge) {
492      if (regionStateStore.hasMergeRegions(ri)) {
493        String msg = "Skip merging " + regionNamesToLog + ", because a parent, "
494          + RegionInfo.getShortNameToLog(ri) + ", has a merge qualifier "
495          + "(if a 'merge column' in parent, it was recently merged but still has outstanding "
496          + "references to its parents that must be cleared before it can participate in merge -- "
497          + "major compact it to hurry clearing of its references)";
498        LOG.warn(msg);
499        throw new MergeRegionException(msg);
500      }
501      RegionState state = regionStates.getRegionState(ri.getEncodedName());
502      if (state == null) {
503        throw new UnknownRegionException(
504          RegionInfo.getShortNameToLog(ri) + " UNKNOWN (Has it been garbage collected?)");
505      }
506      if (!state.isOpened()) {
507        throw new MergeRegionException("Unable to merge regions that are NOT online: " + ri);
508      }
509      // Ask the remote regionserver if regions are mergeable. If we get an IOE, report it
510      // along with the failure, so we can see why regions are not mergeable at this time.
511      try {
512        if (!isMergeable(env, state)) {
513          setFailure(getClass().getSimpleName(),
514            new MergeRegionException("Skip merging " + regionNamesToLog + ", because a parent, "
515              + RegionInfo.getShortNameToLog(ri) + ", is not mergeable"));
516          return false;
517        }
518      } catch (IOException e) {
519        IOException ioe = new IOException(RegionInfo.getShortNameToLog(ri) + " NOT mergeable", e);
520        setFailure(getClass().getSimpleName(), ioe);
521        return false;
522      }
523    }
524
525    // Update region states to Merging
526    setRegionStateToMerging(env);
527    return true;
528  }
529
530  private boolean isMergeable(final MasterProcedureEnv env, final RegionState rs)
531    throws IOException {
532    GetRegionInfoResponse response =
533      AssignmentManagerUtil.getRegionInfoResponse(env, rs.getServerName(), rs.getRegion());
534    return response.hasMergeable() && response.getMergeable();
535  }
536
537  /**
538   * Action for rollback a merge table after prepare merge
539   */
540  private void rollbackPrepareMerge(final MasterProcedureEnv env) throws IOException {
541    for (RegionInfo rinfo : regionsToMerge) {
542      RegionStateNode regionStateNode =
543        env.getAssignmentManager().getRegionStates().getRegionStateNode(rinfo);
544      if (regionStateNode.getState() == State.MERGING) {
545        regionStateNode.setState(State.OPEN);
546      }
547    }
548  }
549
550  /**
551   * Pre merge region action
552   * @param env MasterProcedureEnv
553   **/
554  private void preMergeRegions(final MasterProcedureEnv env) throws IOException {
555    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
556    if (cpHost != null) {
557      cpHost.preMergeRegionsAction(regionsToMerge, getUser());
558    }
559    // TODO: Clean up split and merge. Currently all over the place.
560    try {
561      MasterQuotaManager masterQuotaManager = env.getMasterServices().getMasterQuotaManager();
562      if (masterQuotaManager != null) {
563        masterQuotaManager.onRegionMerged(this.mergedRegion);
564      }
565    } catch (QuotaExceededException e) {
566      // TODO: why is this here? merge requests can be submitted by actors other than the normalizer
567      env.getMasterServices().getRegionNormalizerManager()
568        .planSkipped(NormalizationPlan.PlanType.MERGE);
569      throw e;
570    }
571  }
572
573  /**
574   * Action after rollback a merge table regions action.
575   */
576  private void postRollBackMergeRegions(final MasterProcedureEnv env) throws IOException {
577    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
578    if (cpHost != null) {
579      cpHost.postRollBackMergeRegionsAction(regionsToMerge, getUser());
580    }
581  }
582
583  /**
584   * Set the region states to MERGING state
585   */
586  private void setRegionStateToMerging(final MasterProcedureEnv env) {
587    // Set State.MERGING to regions to be merged
588    RegionStates regionStates = env.getAssignmentManager().getRegionStates();
589    for (RegionInfo ri : this.regionsToMerge) {
590      regionStates.getRegionStateNode(ri).setState(State.MERGING);
591    }
592  }
593
594  /**
595   * Create merged region. The way the merge works is that we make a 'merges' temporary directory in
596   * the FIRST parent region to merge (Do not change this without also changing the rollback where
597   * we look in this FIRST region for the merge dir). We then collect here references to all the
598   * store files in all the parent regions including those of the FIRST parent region into a
599   * subdirectory, named for the resultant merged region. We then call commitMergeRegion. It finds
600   * this subdirectory of storefile references and moves them under the new merge region (creating
601   * the region layout as side effect). After assign of the new merge region, we will run a
602   * compaction. This will undo the references but the reference files remain in place until the
603   * archiver runs (which it does on a period as a chore in the RegionServer that hosts the merge
604   * region -- see CompactedHFilesDischarger). Once the archiver has moved aside the no-longer used
605   * references, the merge region no longer has references. The catalog janitor will notice when it
606   * runs next and it will remove the old parent regions.
607   */
608  private void createMergedRegion(final MasterProcedureEnv env) throws IOException {
609    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
610    final Path tableDir = CommonFSUtils.getTableDir(mfs.getRootDir(), regionsToMerge[0].getTable());
611    final FileSystem fs = mfs.getFileSystem();
612    List<StoreFileInfo> mergedFiles = new ArrayList<StoreFileInfo>();
613    HRegionFileSystem mergeRegionFs = HRegionFileSystem
614      .createRegionOnFileSystem(env.getMasterConfiguration(), fs, tableDir, mergedRegion);
615
616    for (RegionInfo ri : this.regionsToMerge) {
617      HRegionFileSystem regionFs = HRegionFileSystem
618        .openRegionFromFileSystem(env.getMasterConfiguration(), fs, tableDir, ri, false);
619      mergedFiles.addAll(mergeStoreFiles(env, regionFs, mergeRegionFs, mergedRegion));
620    }
621    assert mergeRegionFs != null;
622    mergeRegionFs.commitMergedRegion(mergedFiles, env);
623
624    // Prepare to create merged regions
625    env.getAssignmentManager().getRegionStates().getOrCreateRegionStateNode(mergedRegion)
626      .setState(State.MERGING_NEW);
627  }
628
629  private List<StoreFileInfo> mergeStoreFiles(MasterProcedureEnv env, HRegionFileSystem regionFs,
630    HRegionFileSystem mergeRegionFs, RegionInfo mergedRegion) throws IOException {
631    final TableDescriptor htd =
632      env.getMasterServices().getTableDescriptors().get(mergedRegion.getTable());
633    List<StoreFileInfo> mergedFiles = new ArrayList<StoreFileInfo>();
634    for (ColumnFamilyDescriptor hcd : htd.getColumnFamilies()) {
635      String family = hcd.getNameAsString();
636      StoreFileTracker tracker =
637        StoreFileTrackerFactory.create(env.getMasterConfiguration(), htd, hcd, regionFs);
638      final Collection<StoreFileInfo> storeFiles = tracker.load();
639      if (storeFiles != null && storeFiles.size() > 0) {
640        final Configuration storeConfiguration =
641          StoreUtils.createStoreConfiguration(env.getMasterConfiguration(), htd, hcd);
642        for (StoreFileInfo storeFileInfo : storeFiles) {
643          // Create reference file(s) to parent region file here in mergedDir.
644          // As this procedure is running on master, use CacheConfig.DISABLED means
645          // don't cache any block.
646          // We also need to pass through a suitable CompoundConfiguration as if this
647          // is running in a regionserver's Store context, or we might not be able
648          // to read the hfiles.
649          storeFileInfo.setConf(storeConfiguration);
650          StoreFileInfo refFile = mergeRegionFs.mergeStoreFile(regionFs.getRegionInfo(), family,
651            new HStoreFile(storeFileInfo, hcd.getBloomFilterType(), CacheConfig.DISABLED), tracker);
652          mergedFiles.add(refFile);
653        }
654      }
655    }
656    return mergedFiles;
657  }
658
659  /**
660   * Clean up a merged region on rollback after failure.
661   */
662  private void cleanupMergedRegion(final MasterProcedureEnv env) throws IOException {
663    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
664    TableName tn = this.regionsToMerge[0].getTable();
665    final Path tabledir = CommonFSUtils.getTableDir(mfs.getRootDir(), tn);
666    final FileSystem fs = mfs.getFileSystem();
667    // See createMergedRegion above where we specify the merge dir as being in the
668    // FIRST merge parent region.
669    HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem(
670      env.getMasterConfiguration(), fs, tabledir, regionsToMerge[0], false);
671    regionFs.cleanupMergedRegion(mergedRegion);
672  }
673
674  /**
675   * Rollback close regions
676   **/
677  private void rollbackCloseRegionsForMerge(MasterProcedureEnv env) throws IOException {
678    // At this point we should check if region was actually closed. If it was not closed then we
679    // don't need to reopen the region and we can just change the regionNode state to OPEN.
680    // if it is already closed then we need to do a reopen of region
681    List<RegionInfo> toAssign = new ArrayList<>();
682    for (RegionInfo rinfo : regionsToMerge) {
683      RegionStateNode regionStateNode =
684        env.getAssignmentManager().getRegionStates().getRegionStateNode(rinfo);
685      if (regionStateNode.getState() != State.MERGING) {
686        // same as before HBASE-28405
687        toAssign.add(rinfo);
688      }
689    }
690    AssignmentManagerUtil.reopenRegionsForRollback(env, toAssign, getRegionReplication(env),
691      getServerName(env));
692  }
693
694  private TransitRegionStateProcedure[] createUnassignProcedures(MasterProcedureEnv env)
695    throws IOException {
696    return AssignmentManagerUtil.createUnassignProceduresForSplitOrMerge(env,
697      Stream.of(regionsToMerge), getRegionReplication(env));
698  }
699
700  private TransitRegionStateProcedure[] createAssignProcedures(MasterProcedureEnv env)
701    throws IOException {
702    return AssignmentManagerUtil.createAssignProceduresForOpeningNewRegions(env,
703      Collections.singletonList(mergedRegion), getRegionReplication(env), getServerName(env));
704  }
705
706  private int getRegionReplication(final MasterProcedureEnv env) throws IOException {
707    return env.getMasterServices().getTableDescriptors().get(getTableName()).getRegionReplication();
708  }
709
710  /**
711   * Post merge region action
712   * @param env MasterProcedureEnv
713   **/
714  private void preMergeRegionsCommit(final MasterProcedureEnv env) throws IOException {
715    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
716    if (cpHost != null) {
717      @MetaMutationAnnotation
718      final List<Mutation> metaEntries = new ArrayList<>();
719      cpHost.preMergeRegionsCommit(regionsToMerge, metaEntries, getUser());
720      try {
721        for (Mutation p : metaEntries) {
722          RegionInfo.parseRegionName(p.getRow());
723        }
724      } catch (IOException e) {
725        LOG.error(
726          "Row key of mutation from coprocessor is not parsable as region name. "
727            + "Mutations from coprocessor should only be for {} table.",
728          TableName.META_TABLE_NAME, e);
729        throw e;
730      }
731    }
732  }
733
734  /**
735   * Add merged region to META and delete original regions.
736   */
737  private void updateMetaForMergedRegions(final MasterProcedureEnv env) throws IOException {
738    env.getAssignmentManager().markRegionAsMerged(mergedRegion, getServerName(env),
739      this.regionsToMerge);
740  }
741
742  /**
743   * Post merge region action
744   * @param env MasterProcedureEnv
745   **/
746  private void postMergeRegionsCommit(final MasterProcedureEnv env) throws IOException {
747    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
748    if (cpHost != null) {
749      cpHost.postMergeRegionsCommit(regionsToMerge, mergedRegion, getUser());
750    }
751  }
752
753  /**
754   * Post merge region action
755   * @param env MasterProcedureEnv
756   **/
757  private void postCompletedMergeRegions(final MasterProcedureEnv env) throws IOException {
758    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
759    if (cpHost != null) {
760      cpHost.postCompletedMergeRegionsAction(regionsToMerge, mergedRegion, getUser());
761    }
762  }
763
764  /**
765   * The procedure could be restarted from a different machine. If the variable is null, we need to
766   * retrieve it.
767   * @param env MasterProcedureEnv
768   */
769  private ServerName getServerName(final MasterProcedureEnv env) {
770    if (regionLocation == null) {
771      regionLocation =
772        env.getAssignmentManager().getRegionStates().getRegionServerOfRegion(regionsToMerge[0]);
773      // May still be null here but return null and let caller deal.
774      // Means we lost the in-memory-only location. We are in recovery
775      // or so. The caller should be able to deal w/ a null ServerName.
776      // Let them go to the Balancer to find one to use instead.
777    }
778    return regionLocation;
779  }
780
781  private void writeMaxSequenceIdFile(MasterProcedureEnv env) throws IOException {
782    MasterFileSystem fs = env.getMasterFileSystem();
783    long maxSequenceId = -1L;
784    for (RegionInfo region : regionsToMerge) {
785      maxSequenceId =
786        Math.max(maxSequenceId, WALSplitUtil.getMaxRegionSequenceId(env.getMasterConfiguration(),
787          region, fs::getFileSystem, fs::getWALFileSystem));
788    }
789    if (maxSequenceId > 0) {
790      WALSplitUtil.writeRegionSequenceIdFile(fs.getWALFileSystem(),
791        getWALRegionDir(env, mergedRegion), maxSequenceId);
792    }
793  }
794
795  /** Returns The merged region. Maybe be null if called to early or we failed. */
796  RegionInfo getMergedRegion() {
797    return this.mergedRegion;
798  }
799
800  @Override
801  protected boolean abort(MasterProcedureEnv env) {
802    // Abort means rollback. We can't rollback all steps. HBASE-18018 added abort to all
803    // Procedures. Here is a Procedure that has a PONR and cannot be aborted once it enters this
804    // range of steps; what do we do for these should an operator want to cancel them? HBASE-20022.
805    return isRollbackSupported(getCurrentState()) && super.abort(env);
806  }
807}