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.CatalogJanitor;
044import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
045import org.apache.hadoop.hbase.master.MasterFileSystem;
046import org.apache.hadoop.hbase.master.RegionState;
047import org.apache.hadoop.hbase.master.RegionState.State;
048import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan;
049import org.apache.hadoop.hbase.master.procedure.AbstractStateMachineTableProcedure;
050import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
051import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil;
052import org.apache.hadoop.hbase.procedure2.ProcedureMetrics;
053import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
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.util.Bytes;
059import org.apache.hadoop.hbase.util.FSUtils;
060import org.apache.hadoop.hbase.wal.WALSplitUtil;
061import org.apache.yetus.audience.InterfaceAudience;
062import org.slf4j.Logger;
063import org.slf4j.LoggerFactory;
064
065import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
066
067import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
068import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoResponse;
069import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
070import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.MergeTableRegionsState;
071
072/**
073 * The procedure to Merge regions in a table. This procedure takes an exclusive table
074 * lock since it is working over multiple regions. It holds the lock for the life of the procedure.
075 * Throws exception on construction if determines context hostile to merge (cluster going down or
076 * master is shutting down or table is disabled).
077 */
078@InterfaceAudience.Private
079public class MergeTableRegionsProcedure
080    extends AbstractStateMachineTableProcedure<MergeTableRegionsState> {
081  private static final Logger LOG = LoggerFactory.getLogger(MergeTableRegionsProcedure.class);
082  private ServerName regionLocation;
083
084  /**
085   * Two or more regions to merge, the 'merge parents'.
086   */
087  private RegionInfo[] regionsToMerge;
088
089  /**
090   * The resulting merged region.
091   */
092  private RegionInfo mergedRegion;
093
094  private boolean force;
095
096  public MergeTableRegionsProcedure() {
097    // Required by the Procedure framework to create the procedure on replay
098  }
099
100  public MergeTableRegionsProcedure(final MasterProcedureEnv env,
101      final RegionInfo[] regionsToMerge, final boolean force)
102      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>
160   * to figure what extremes for start and end keys to use; merged region needs
161   * to have an extent sufficient to 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()).
184        setStartKey(lowestStartKey).
185        setEndKey(highestEndKey).
186        setSplit(false).
187        setRegionId(highestRegionId + 1/*Add one so new merged region is highest*/).
188        build();
189  }
190
191  @Override
192  protected Flow executeFromState(final MasterProcedureEnv env,
193      MergeTableRegionsState state) {
194    LOG.trace("{} execute state={}", this, state);
195    try {
196      switch (state) {
197        case MERGE_TABLE_REGIONS_PREPARE:
198          if (!prepareMergeRegion(env)) {
199            assert isFailed() : "Merge region should have an exception here";
200            return Flow.NO_MORE_STATE;
201          }
202          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION);
203          break;
204        case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION:
205          preMergeRegions(env);
206          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CLOSE_REGIONS);
207          break;
208        case MERGE_TABLE_REGIONS_CLOSE_REGIONS:
209          addChildProcedure(createUnassignProcedures(env));
210          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS);
211          break;
212        case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
213          checkClosedRegions(env);
214          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CREATE_MERGED_REGION);
215          break;
216        case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION:
217          removeNonDefaultReplicas(env);
218          createMergedRegion(env);
219          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE);
220          break;
221        case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:
222          writeMaxSequenceIdFile(env);
223          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION);
224          break;
225        case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION:
226          preMergeRegionsCommit(env);
227          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_UPDATE_META);
228          break;
229        case MERGE_TABLE_REGIONS_UPDATE_META:
230          updateMetaForMergedRegions(env);
231          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION);
232          break;
233        case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
234          postMergeRegionsCommit(env);
235          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_OPEN_MERGED_REGION);
236          break;
237        case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
238          addChildProcedure(createAssignProcedures(env));
239          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_OPERATION);
240          break;
241        case MERGE_TABLE_REGIONS_POST_OPERATION:
242          postCompletedMergeRegions(env);
243          return Flow.NO_MORE_STATE;
244        default:
245          throw new UnsupportedOperationException(this + " unhandled state=" + state);
246      }
247    } catch (IOException e) {
248      String msg = "Error trying to merge " + RegionInfo.getShortNameToLog(regionsToMerge) +
249          " in " + getTableName() + " (in state=" + state + ")";
250      if (!isRollbackSupported(state)) {
251        // We reach a state that cannot be rolled back. We just need to keep retrying.
252        LOG.warn(msg, e);
253      } else {
254        LOG.error(msg, e);
255        setFailure("master-merge-regions", e);
256      }
257    }
258    return Flow.HAS_MORE_STATE;
259  }
260
261  /**
262   * To rollback {@link MergeTableRegionsProcedure}, two AssignProcedures are asynchronously
263   * submitted for each region to be merged (rollback doesn't wait on the completion of the
264   * AssignProcedures) . This can be improved by changing rollback() to support sub-procedures.
265   * See HBASE-19851 for details.
266   */
267  @Override
268  protected void rollbackState(final MasterProcedureEnv env, final MergeTableRegionsState state)
269      throws IOException {
270    LOG.trace("{} rollback state={}", this, state);
271
272    try {
273      switch (state) {
274        case MERGE_TABLE_REGIONS_POST_OPERATION:
275        case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
276        case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
277        case MERGE_TABLE_REGIONS_UPDATE_META:
278          String msg = this + " We are in the " + state + " state." +
279            " It is complicated to rollback the merge operation that region server is working on." +
280            " Rollback is not supported and we should let the merge operation to complete";
281          LOG.warn(msg);
282          // PONR
283          throw new UnsupportedOperationException(this + " unhandled state=" + state);
284        case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION:
285          break;
286        case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION:
287        case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:
288          cleanupMergedRegion(env);
289          break;
290        case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
291          break;
292        case MERGE_TABLE_REGIONS_CLOSE_REGIONS:
293          rollbackCloseRegionsForMerge(env);
294          break;
295        case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION:
296          postRollBackMergeRegions(env);
297          break;
298        case MERGE_TABLE_REGIONS_PREPARE:
299          break;
300        default:
301          throw new UnsupportedOperationException(this + " unhandled state=" + state);
302      }
303    } catch (Exception e) {
304      // This will be retried. Unless there is a bug in the code,
305      // this should be just a "temporary error" (e.g. network down)
306      LOG.warn("Failed rollback attempt step " + state + " for merging the regions "
307          + RegionInfo.getShortNameToLog(regionsToMerge) + " in table " + getTableName(), e);
308      throw e;
309    }
310  }
311
312  /*
313   * Check whether we are in the state that can be rolled back
314   */
315  @Override
316  protected boolean isRollbackSupported(final MergeTableRegionsState state) {
317    switch (state) {
318      case MERGE_TABLE_REGIONS_POST_OPERATION:
319      case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
320      case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
321      case MERGE_TABLE_REGIONS_UPDATE_META:
322        // It is not safe to rollback in these states.
323        return false;
324      default:
325        break;
326    }
327    return true;
328  }
329
330  private void removeNonDefaultReplicas(MasterProcedureEnv env) throws IOException {
331    AssignmentManagerUtil.removeNonDefaultReplicas(env, Stream.of(regionsToMerge),
332        getRegionReplication(env));
333  }
334
335  private void checkClosedRegions(MasterProcedureEnv env) throws IOException {
336    // Theoretically this should not happen any more after we use TRSP, but anyway
337    // let's add a check here
338    for (RegionInfo region : regionsToMerge) {
339      AssignmentManagerUtil.checkClosedRegion(env, region);
340    }
341  }
342
343  @Override
344  protected MergeTableRegionsState getState(final int stateId) {
345    return MergeTableRegionsState.forNumber(stateId);
346  }
347
348  @Override
349  protected int getStateId(final MergeTableRegionsState state) {
350    return state.getNumber();
351  }
352
353  @Override
354  protected MergeTableRegionsState getInitialState() {
355    return MergeTableRegionsState.MERGE_TABLE_REGIONS_PREPARE;
356  }
357
358  @Override
359  protected void serializeStateData(ProcedureStateSerializer serializer)
360      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))
367        .setForcible(force);
368    for (RegionInfo ri: regionsToMerge) {
369      mergeTableRegionsMsg.addRegionInfo(ProtobufUtil.toRegionInfo(ri));
370    }
371    serializer.serialize(mergeTableRegionsMsg.build());
372  }
373
374  @Override
375  protected void deserializeStateData(ProcedureStateSerializer serializer)
376      throws IOException {
377    super.deserializeStateData(serializer);
378
379    final MasterProcedureProtos.MergeTableRegionsStateData mergeTableRegionsMsg =
380        serializer.deserialize(MasterProcedureProtos.MergeTableRegionsStateData.class);
381    setUser(MasterProcedureUtil.toUserInfo(mergeTableRegionsMsg.getUserInfo()));
382
383    assert(mergeTableRegionsMsg.getRegionInfoCount() == 2);
384    regionsToMerge = new RegionInfo[mergeTableRegionsMsg.getRegionInfoCount()];
385    for (int i = 0; i < regionsToMerge.length; i++) {
386      regionsToMerge[i] = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getRegionInfo(i));
387    }
388
389    mergedRegion = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getMergedRegionInfo());
390  }
391
392  @Override
393  public void toStringClassDetails(StringBuilder sb) {
394    sb.append(getClass().getSimpleName());
395    sb.append(" table=");
396    sb.append(getTableName());
397    sb.append(", regions=");
398    sb.append(RegionInfo.getShortNameToLog(regionsToMerge));
399    sb.append(", force=");
400    sb.append(force);
401  }
402
403  @Override
404  protected LockState acquireLock(final MasterProcedureEnv env) {
405    RegionInfo[] lockRegions = Arrays.copyOf(regionsToMerge, regionsToMerge.length + 1);
406    lockRegions[lockRegions.length - 1] = mergedRegion;
407
408    if (env.getProcedureScheduler().waitRegions(this, getTableName(), lockRegions)) {
409      try {
410        LOG.debug(LockState.LOCK_EVENT_WAIT + " " + env.getProcedureScheduler().dumpLocks());
411      } catch (IOException e) {
412        // Ignore, just for logging
413      }
414      return LockState.LOCK_EVENT_WAIT;
415    }
416    return LockState.LOCK_ACQUIRED;
417  }
418
419  @Override
420  protected void releaseLock(final MasterProcedureEnv env) {
421    RegionInfo[] lockRegions = Arrays.copyOf(regionsToMerge, regionsToMerge.length + 1);
422    lockRegions[lockRegions.length - 1] = mergedRegion;
423
424    env.getProcedureScheduler().wakeRegions(this, getTableName(), lockRegions);
425  }
426
427  @Override
428  protected boolean holdLock(MasterProcedureEnv env) {
429    return true;
430  }
431
432  @Override
433  public TableName getTableName() {
434    return mergedRegion.getTable();
435  }
436
437  @Override
438  public TableOperationType getTableOperationType() {
439    return TableOperationType.REGION_MERGE;
440  }
441
442  @Override
443  protected ProcedureMetrics getProcedureMetrics(MasterProcedureEnv env) {
444    return env.getAssignmentManager().getAssignmentManagerMetrics().getMergeProcMetrics();
445  }
446
447  /**
448   * Prepare merge and do some check
449   */
450  private boolean prepareMergeRegion(final MasterProcedureEnv env) throws IOException {
451    // Fail if we are taking snapshot for the given table
452    TableName tn = regionsToMerge[0].getTable();
453    if (env.getMasterServices().getSnapshotManager().isTakingSnapshot(tn)) {
454      throw new MergeRegionException("Skip merging regions " +
455          RegionInfo.getShortNameToLog(regionsToMerge) + ", because we are snapshotting " + tn);
456    }
457    if (!env.getMasterServices().isSplitOrMergeEnabled(MasterSwitchType.MERGE)) {
458      String regionsStr = Arrays.deepToString(this.regionsToMerge);
459      LOG.warn("Merge switch is off! skip merge of " + regionsStr);
460      super.setFailure(getClass().getSimpleName(),
461          new IOException("Merge of " + regionsStr + " failed because merge switch is off"));
462      return false;
463    }
464
465    if (!env.getMasterServices().getTableDescriptors().get(getTableName()).isMergeEnabled()) {
466      String regionsStr = Arrays.deepToString(regionsToMerge);
467      LOG.warn("Merge is disabled for the table! Skipping merge of {}", regionsStr);
468      super.setFailure(getClass().getSimpleName(), new IOException(
469          "Merge of " + regionsStr + " failed as region merge is disabled for the table"));
470      return false;
471    }
472
473    CatalogJanitor catalogJanitor = env.getMasterServices().getCatalogJanitor();
474    RegionStates regionStates = env.getAssignmentManager().getRegionStates();
475    for (RegionInfo ri: this.regionsToMerge) {
476      if (!catalogJanitor.cleanMergeQualifier(ri)) {
477        String msg = "Skip merging " + RegionInfo.getShortNameToLog(regionsToMerge) +
478            ", because parent " + RegionInfo.getShortNameToLog(ri) + " has a merge qualifier";
479        LOG.warn(msg);
480        throw new MergeRegionException(msg);
481      }
482      RegionState state = regionStates.getRegionState(ri.getEncodedName());
483      if (state == null) {
484        throw new UnknownRegionException("No state for " + RegionInfo.getShortNameToLog(ri));
485      }
486      if (!state.isOpened()) {
487        throw new MergeRegionException("Unable to merge regions that are not online: " + ri);
488      }
489      // Ask the remote regionserver if regions are mergeable. If we get an IOE, report it
490      // along with the failure, so we can see why regions are not mergeable at this time.
491      try {
492        if (!isMergeable(env, state)) {
493          return false;
494        }
495      } catch (IOException e) {
496        IOException ioe = new IOException(RegionInfo.getShortNameToLog(ri) + " NOT mergeable", e);
497        super.setFailure(getClass().getSimpleName(), ioe);
498        return false;
499      }
500    }
501
502    // Update region states to Merging
503    setRegionStateToMerging(env);
504    return true;
505  }
506
507  private boolean isMergeable(final MasterProcedureEnv env, final RegionState rs)
508      throws IOException {
509    GetRegionInfoResponse response =
510      AssignmentManagerUtil.getRegionInfoResponse(env, rs.getServerName(), rs.getRegion());
511    return response.hasMergeable() && response.getMergeable();
512  }
513
514  /**
515   * Pre merge region action
516   * @param env MasterProcedureEnv
517   **/
518  private void preMergeRegions(final MasterProcedureEnv env) throws IOException {
519    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
520    if (cpHost != null) {
521      cpHost.preMergeRegionsAction(regionsToMerge, getUser());
522    }
523    // TODO: Clean up split and merge. Currently all over the place.
524    try {
525      env.getMasterServices().getMasterQuotaManager().onRegionMerged(this.mergedRegion);
526    } catch (QuotaExceededException e) {
527      env.getMasterServices().getRegionNormalizer().planSkipped(this.mergedRegion,
528          NormalizationPlan.PlanType.MERGE);
529      throw e;
530    }
531  }
532
533  /**
534   * Action after rollback a merge table regions action.
535   */
536  private void postRollBackMergeRegions(final MasterProcedureEnv env) throws IOException {
537    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
538    if (cpHost != null) {
539      cpHost.postRollBackMergeRegionsAction(regionsToMerge, getUser());
540    }
541  }
542
543  /**
544   * Set the region states to MERGING state
545   */
546  private void setRegionStateToMerging(final MasterProcedureEnv env) {
547    // Set State.MERGING to regions to be merged
548    RegionStates regionStates = env.getAssignmentManager().getRegionStates();
549    for (RegionInfo ri: this.regionsToMerge) {
550      regionStates.getRegionStateNode(ri).setState(State.MERGING);
551    }
552  }
553
554  /**
555   * Create merged region.
556   * The way the merge works is that we make a 'merges' temporary
557   * directory in the FIRST parent region to merge (Do not change this without
558   * also changing the rollback where we look in this FIRST region for the
559   * merge dir). We then collect here references to all the store files in all
560   * the parent regions including those of the FIRST parent region into a
561   * subdirectory, named for the resultant merged region. We then call
562   * commitMergeRegion. It finds this subdirectory of storefile references
563   * and moves them under the new merge region (creating the region layout
564   * as side effect). After assign of the new merge region, we will run a
565   * compaction. This will undo the references but the reference files remain
566   * in place until the archiver runs (which it does on a period as a chore
567   * in the RegionServer that hosts the merge region -- see
568   * CompactedHFilesDischarger). Once the archiver has moved aside the
569   * no-longer used references, the merge region no longer has references.
570   * The catalog janitor will notice when it runs next and it will remove
571   * the old parent regions.
572   */
573  private void createMergedRegion(final MasterProcedureEnv env) throws IOException {
574    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
575    final Path tabledir = FSUtils.getTableDir(mfs.getRootDir(), regionsToMerge[0].getTable());
576    final FileSystem fs = mfs.getFileSystem();
577    HRegionFileSystem mergeRegionFs = null;
578    for (RegionInfo ri: this.regionsToMerge) {
579      HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem(
580          env.getMasterConfiguration(), fs, tabledir, ri, false);
581      if (mergeRegionFs == null) {
582        mergeRegionFs = regionFs;
583        mergeRegionFs.createMergesDir();
584      }
585      mergeStoreFiles(env, regionFs, mergeRegionFs.getMergesDir());
586    }
587    assert mergeRegionFs != null;
588    mergeRegionFs.commitMergedRegion(mergedRegion);
589
590    // Prepare to create merged regions
591    env.getAssignmentManager().getRegionStates().
592        getOrCreateRegionStateNode(mergedRegion).setState(State.MERGING_NEW);
593  }
594
595  /**
596   * Create reference file(s) to parent region hfiles in the <code>mergeDir</code>
597   * @param regionFs merge parent region file system
598   * @param mergeDir the temp directory in which we are accumulating references.
599   */
600  private void mergeStoreFiles(final MasterProcedureEnv env, final HRegionFileSystem regionFs,
601      final Path mergeDir) throws IOException {
602    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
603    final Configuration conf = env.getMasterConfiguration();
604    final TableDescriptor htd = env.getMasterServices().getTableDescriptors().get(getTableName());
605    for (ColumnFamilyDescriptor hcd : htd.getColumnFamilies()) {
606      String family = hcd.getNameAsString();
607      final Collection<StoreFileInfo> storeFiles = regionFs.getStoreFiles(family);
608      if (storeFiles != null && storeFiles.size() > 0) {
609        for (StoreFileInfo storeFileInfo : storeFiles) {
610          // Create reference file(s) to parent region file here in mergedDir.
611          // As this procedure is running on master, use CacheConfig.DISABLED means
612          // don't cache any block.
613          regionFs.mergeStoreFile(mergedRegion, family, new HStoreFile(mfs.getFileSystem(),
614              storeFileInfo, conf, CacheConfig.DISABLED, hcd.getBloomFilterType(), true),
615            mergeDir);
616        }
617      }
618    }
619  }
620
621  /**
622   * Clean up a merged region on rollback after failure.
623   */
624  private void cleanupMergedRegion(final MasterProcedureEnv env) throws IOException {
625    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
626    TableName tn = this.regionsToMerge[0].getTable();
627    final Path tabledir = FSUtils.getTableDir(mfs.getRootDir(), tn);
628    final FileSystem fs = mfs.getFileSystem();
629    // See createMergedRegion above where we specify the merge dir as being in the
630    // FIRST merge parent region.
631    HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem(
632      env.getMasterConfiguration(), fs, tabledir, regionsToMerge[0], false);
633    regionFs.cleanupMergedRegion(mergedRegion);
634  }
635
636  /**
637   * Rollback close regions
638   **/
639  private void rollbackCloseRegionsForMerge(MasterProcedureEnv env) throws IOException {
640    AssignmentManagerUtil.reopenRegionsForRollback(env, Arrays.asList(regionsToMerge),
641      getRegionReplication(env), getServerName(env));
642  }
643
644  private TransitRegionStateProcedure[] createUnassignProcedures(MasterProcedureEnv env)
645      throws IOException {
646    return AssignmentManagerUtil.createUnassignProceduresForSplitOrMerge(env,
647      Stream.of(regionsToMerge), getRegionReplication(env));
648  }
649
650  private TransitRegionStateProcedure[] createAssignProcedures(MasterProcedureEnv env)
651      throws IOException {
652    return AssignmentManagerUtil.createAssignProceduresForOpeningNewRegions(env,
653      Collections.singletonList(mergedRegion), getRegionReplication(env), getServerName(env));
654  }
655
656  private int getRegionReplication(final MasterProcedureEnv env) throws IOException {
657    return env.getMasterServices().getTableDescriptors().get(getTableName()).
658        getRegionReplication();
659  }
660
661  /**
662   * Post merge region action
663   * @param env MasterProcedureEnv
664   **/
665  private void preMergeRegionsCommit(final MasterProcedureEnv env) throws IOException {
666    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
667    if (cpHost != null) {
668      @MetaMutationAnnotation
669      final List<Mutation> metaEntries = new ArrayList<>();
670      cpHost.preMergeRegionsCommit(regionsToMerge, metaEntries, getUser());
671      try {
672        for (Mutation p : metaEntries) {
673          RegionInfo.parseRegionName(p.getRow());
674        }
675      } catch (IOException e) {
676        LOG.error("Row key of mutation from coprocessor is not parsable as region name. "
677          + "Mutations from coprocessor should only be for hbase:meta table.", e);
678        throw e;
679      }
680    }
681  }
682
683  /**
684   * Add merged region to META and delete original regions.
685   */
686  private void updateMetaForMergedRegions(final MasterProcedureEnv env) throws IOException {
687    env.getAssignmentManager().markRegionAsMerged(mergedRegion, getServerName(env),
688        this.regionsToMerge);
689  }
690
691  /**
692   * Post merge region action
693   * @param env MasterProcedureEnv
694   **/
695  private void postMergeRegionsCommit(final MasterProcedureEnv env) throws IOException {
696    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
697    if (cpHost != null) {
698      cpHost.postMergeRegionsCommit(regionsToMerge, mergedRegion, getUser());
699    }
700  }
701
702  /**
703   * Post merge region action
704   * @param env MasterProcedureEnv
705   **/
706  private void postCompletedMergeRegions(final MasterProcedureEnv env) throws IOException {
707    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
708    if (cpHost != null) {
709      cpHost.postCompletedMergeRegionsAction(regionsToMerge, mergedRegion, getUser());
710    }
711  }
712
713  /**
714   * The procedure could be restarted from a different machine. If the variable is null, we need to
715   * retrieve it.
716   * @param env MasterProcedureEnv
717   * @return serverName
718   */
719  private ServerName getServerName(final MasterProcedureEnv env) {
720    if (regionLocation == null) {
721      regionLocation = env.getAssignmentManager().getRegionStates().
722          getRegionServerOfRegion(regionsToMerge[0]);
723      // May still be null here but return null and let caller deal.
724      // Means we lost the in-memory-only location. We are in recovery
725      // or so. The caller should be able to deal w/ a null ServerName.
726      // Let them go to the Balancer to find one to use instead.
727    }
728    return regionLocation;
729  }
730
731  private void writeMaxSequenceIdFile(MasterProcedureEnv env) throws IOException {
732    MasterFileSystem fs = env.getMasterFileSystem();
733    long maxSequenceId = -1L;
734    for (RegionInfo region : regionsToMerge) {
735      maxSequenceId =
736        Math.max(maxSequenceId, WALSplitUtil.getMaxRegionSequenceId(env.getMasterConfiguration(),
737          region, fs::getFileSystem, fs::getWALFileSystem));
738    }
739    if (maxSequenceId > 0) {
740      WALSplitUtil.writeRegionSequenceIdFile(fs.getWALFileSystem(),
741        getWALRegionDir(env, mergedRegion), maxSequenceId);
742    }
743  }
744
745  /**
746   * @return The merged region. Maybe be null if called to early or we failed.
747   */
748  @VisibleForTesting
749  RegionInfo getMergedRegion() {
750    return this.mergedRegion;
751  }
752
753  @Override
754  protected boolean abort(MasterProcedureEnv env) {
755    // Abort means rollback. We can't rollback all steps. HBASE-18018 added abort to all
756    // Procedures. Here is a Procedure that has a PONR and cannot be aborted once it enters this
757    // range of steps; what do we do for these should an operator want to cancel them? HBASE-20022.
758    return isRollbackSupported(getCurrentState()) && super.abort(env);
759  }
760}