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