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 */
018
019package org.apache.hadoop.hbase.master.assignment;
020
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.List;
026import java.util.stream.Collectors;
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.RegionReplicaUtil;
041import org.apache.hadoop.hbase.client.TableDescriptor;
042import org.apache.hadoop.hbase.exceptions.MergeRegionException;
043import org.apache.hadoop.hbase.io.hfile.CacheConfig;
044import org.apache.hadoop.hbase.master.CatalogJanitor;
045import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
046import org.apache.hadoop.hbase.master.MasterFileSystem;
047import org.apache.hadoop.hbase.master.RegionState;
048import org.apache.hadoop.hbase.master.RegionState.State;
049import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan;
050import org.apache.hadoop.hbase.master.procedure.AbstractStateMachineTableProcedure;
051import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
052import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil;
053import org.apache.hadoop.hbase.master.procedure.TableQueue;
054import org.apache.hadoop.hbase.procedure2.ProcedureMetrics;
055import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
056import org.apache.hadoop.hbase.quotas.QuotaExceededException;
057import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
058import org.apache.hadoop.hbase.regionserver.HStoreFile;
059import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
060import org.apache.hadoop.hbase.util.Bytes;
061import org.apache.hadoop.hbase.util.FSUtils;
062import org.apache.hadoop.hbase.wal.WALSplitter;
063import org.apache.yetus.audience.InterfaceAudience;
064import org.slf4j.Logger;
065import org.slf4j.LoggerFactory;
066
067import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
068
069import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
070import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoResponse;
071import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
072import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.MergeTableRegionsState;
073
074/**
075 * The procedure to Merge regions in a table. This procedure takes an exclusive table
076 * lock since it is working over multiple regions. It holds the lock for the life of the procedure.
077 * Throws exception on construction if determines context hostile to merge (cluster going down or
078 * master is shutting down or table is disabled).
079 */
080@InterfaceAudience.Private
081public class MergeTableRegionsProcedure
082    extends AbstractStateMachineTableProcedure<MergeTableRegionsState> {
083  private static final Logger LOG = LoggerFactory.getLogger(MergeTableRegionsProcedure.class);
084  private ServerName regionLocation;
085
086  /**
087   * Two or more regions to merge, the 'merge parents'.
088   */
089  private RegionInfo[] regionsToMerge;
090
091  /**
092   * The resulting merged region.
093   */
094  private RegionInfo mergedRegion;
095
096  private boolean force;
097
098  public MergeTableRegionsProcedure() {
099    // Required by the Procedure framework to create the procedure on replay
100  }
101
102  public MergeTableRegionsProcedure(final MasterProcedureEnv env,
103      final RegionInfo[] regionsToMerge, final boolean force)
104      throws IOException {
105    super(env);
106    // Check parent regions. Make sure valid before starting work.
107    // This check calls the super method #checkOnline also.
108    checkRegionsToMerge(env, regionsToMerge, force);
109    // Sort the regions going into the merge.
110    Arrays.sort(regionsToMerge);
111    this.regionsToMerge = regionsToMerge;
112    this.mergedRegion = createMergedRegionInfo(regionsToMerge);
113    // Preflight depends on mergedRegion being set (at least).
114    preflightChecks(env, true);
115    this.force = force;
116  }
117
118  /**
119   * @throws MergeRegionException If unable to merge regions for whatever reasons.
120   */
121  private static void checkRegionsToMerge(MasterProcedureEnv env, final RegionInfo[] regions,
122      final boolean force) throws MergeRegionException {
123    long count = Arrays.stream(regions).distinct().count();
124    if (regions.length != count) {
125      throw new MergeRegionException("Duplicate regions specified; cannot merge a region to " +
126          "itself. Passed in " + regions.length + " but only " + count + " unique.");
127    }
128    if (count < 2) {
129      throw new MergeRegionException("Need two Regions at least to run a Merge");
130    }
131    RegionInfo previous = null;
132    for (RegionInfo ri: regions) {
133      if (previous != null) {
134        if (!previous.getTable().equals(ri.getTable())) {
135          String msg = "Can't merge regions from different tables: " + previous + ", " + ri;
136          LOG.warn(msg);
137          throw new MergeRegionException(msg);
138        }
139        if (!force && !ri.isAdjacent(previous) && !ri.isOverlap(previous)) {
140          String msg = "Unable to merge non-adjacent or non-overlapping regions " +
141              previous.getShortNameToLog() + ", " + ri.getShortNameToLog() + " when force=false";
142          LOG.warn(msg);
143          throw new MergeRegionException(msg);
144        }
145      }
146
147      if (ri.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
148        throw new MergeRegionException("Can't merge non-default replicas; " + ri);
149      }
150      try {
151        checkOnline(env, ri);
152      } catch (DoNotRetryRegionException dnrre) {
153        throw new MergeRegionException(dnrre);
154      }
155
156      previous = ri;
157    }
158  }
159
160  /**
161   * Create merged region info by looking at passed in <code>regionsToMerge</code>
162   * to figure what extremes for start and end keys to use; merged region needs
163   * to have an extent sufficient to cover all regions-to-merge.
164   */
165  private static RegionInfo createMergedRegionInfo(final RegionInfo[] regionsToMerge) {
166    byte [] lowestStartKey = null;
167    byte [] highestEndKey = null;
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, getRegionReplication(env)));
210          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS);
211          break;
212        case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
213          List<RegionInfo> ris = hasRecoveredEdits(env);
214          if (ris.isEmpty()) {
215            setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CREATE_MERGED_REGION);
216          } else {
217            // Need to reopen parent regions to pickup missed recovered.edits. Do it by creating
218            // child assigns and then stepping back to MERGE_TABLE_REGIONS_CLOSE_REGIONS.
219            // Just assign the primary regions recovering the missed recovered.edits -- no replicas.
220            // May need to cycle here a few times if heavy writes.
221            // TODO: Add an assign read-only.
222            for (RegionInfo ri: ris) {
223              LOG.info("Found recovered.edits under {}, reopen to pickup missed edits!", ri);
224              addChildProcedure(env.getAssignmentManager().createAssignProcedure(ri));
225            }
226            setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_CLOSE_REGIONS);
227          }
228          break;
229        case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION:
230          createMergedRegion(env);
231          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE);
232          break;
233        case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:
234          writeMaxSequenceIdFile(env);
235          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION);
236          break;
237        case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION:
238          preMergeRegionsCommit(env);
239          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_UPDATE_META);
240          break;
241        case MERGE_TABLE_REGIONS_UPDATE_META:
242          updateMetaForMergedRegions(env);
243          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION);
244          break;
245        case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
246          postMergeRegionsCommit(env);
247          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_OPEN_MERGED_REGION);
248          break;
249        case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
250          addChildProcedure(createAssignProcedures(env, getRegionReplication(env)));
251          setNextState(MergeTableRegionsState.MERGE_TABLE_REGIONS_POST_OPERATION);
252          break;
253        case MERGE_TABLE_REGIONS_POST_OPERATION:
254          postCompletedMergeRegions(env);
255          return Flow.NO_MORE_STATE;
256        default:
257          throw new UnsupportedOperationException(this + " unhandled state=" + state);
258      }
259    } catch (IOException e) {
260      String msg = "Error trying to merge " + RegionInfo.getShortNameToLog(regionsToMerge) +
261          " in " + getTableName() + " (in state=" + state + ")";
262      if (!isRollbackSupported(state)) {
263        // We reach a state that cannot be rolled back. We just need to keep retrying.
264        LOG.warn(msg, e);
265      } else {
266        LOG.error(msg, e);
267        setFailure("master-merge-regions", e);
268      }
269    }
270    return Flow.HAS_MORE_STATE;
271  }
272
273  /**
274   * To rollback {@link MergeTableRegionsProcedure}, two AssignProcedures are asynchronously
275   * submitted for each region to be merged (rollback doesn't wait on the completion of the
276   * AssignProcedures) . This can be improved by changing rollback() to support sub-procedures.
277   * See HBASE-19851 for details.
278   */
279  @Override
280  protected void rollbackState(final MasterProcedureEnv env, final MergeTableRegionsState state)
281      throws IOException {
282    LOG.trace("{} rollback state={}", this, state);
283
284    try {
285      switch (state) {
286        case MERGE_TABLE_REGIONS_POST_OPERATION:
287        case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
288        case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
289        case MERGE_TABLE_REGIONS_UPDATE_META:
290          String msg = this + " We are in the " + state + " state." +
291            " It is complicated to rollback the merge operation that region server is working on." +
292            " Rollback is not supported and we should let the merge operation to complete";
293          LOG.warn(msg);
294          // PONR
295          throw new UnsupportedOperationException(this + " unhandled state=" + state);
296        case MERGE_TABLE_REGIONS_PRE_MERGE_COMMIT_OPERATION:
297          break;
298        case MERGE_TABLE_REGIONS_CREATE_MERGED_REGION:
299        case MERGE_TABLE_REGIONS_WRITE_MAX_SEQUENCE_ID_FILE:
300          cleanupMergedRegion(env);
301          break;
302        case MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
303          break;
304        case MERGE_TABLE_REGIONS_CLOSE_REGIONS:
305          rollbackCloseRegionsForMerge(env);
306          break;
307        case MERGE_TABLE_REGIONS_PRE_MERGE_OPERATION:
308          postRollBackMergeRegions(env);
309          break;
310        case MERGE_TABLE_REGIONS_PREPARE:
311          break;
312        default:
313          throw new UnsupportedOperationException(this + " unhandled state=" + state);
314      }
315    } catch (Exception e) {
316      // This will be retried. Unless there is a bug in the code,
317      // this should be just a "temporary error" (e.g. network down)
318      LOG.warn("Failed rollback attempt step " + state + " for merging the regions "
319          + RegionInfo.getShortNameToLog(regionsToMerge) + " in table " + getTableName(), e);
320      throw e;
321    }
322  }
323
324  /*
325   * Check whether we are in the state that can be rolled back
326   */
327  @Override
328  protected boolean isRollbackSupported(final MergeTableRegionsState state) {
329    switch (state) {
330      case MERGE_TABLE_REGIONS_POST_OPERATION:
331      case MERGE_TABLE_REGIONS_OPEN_MERGED_REGION:
332      case MERGE_TABLE_REGIONS_POST_MERGE_COMMIT_OPERATION:
333      case MERGE_TABLE_REGIONS_UPDATE_META:
334        // It is not safe to rollback in these states.
335        return false;
336      default:
337        break;
338    }
339    return true;
340  }
341
342  @Override
343  protected MergeTableRegionsState getState(final int stateId) {
344    return MergeTableRegionsState.forNumber(stateId);
345  }
346
347  @Override
348  protected int getStateId(final MergeTableRegionsState state) {
349    return state.getNumber();
350  }
351
352  @Override
353  protected MergeTableRegionsState getInitialState() {
354    return MergeTableRegionsState.MERGE_TABLE_REGIONS_PREPARE;
355  }
356
357  @Override
358  protected void serializeStateData(ProcedureStateSerializer serializer)
359      throws IOException {
360    super.serializeStateData(serializer);
361
362    final MasterProcedureProtos.MergeTableRegionsStateData.Builder mergeTableRegionsMsg =
363        MasterProcedureProtos.MergeTableRegionsStateData.newBuilder()
364        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
365        .setMergedRegionInfo(ProtobufUtil.toRegionInfo(mergedRegion))
366        .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)
375      throws IOException {
376    super.deserializeStateData(serializer);
377
378    final MasterProcedureProtos.MergeTableRegionsStateData mergeTableRegionsMsg =
379        serializer.deserialize(MasterProcedureProtos.MergeTableRegionsStateData.class);
380    setUser(MasterProcedureUtil.toUserInfo(mergeTableRegionsMsg.getUserInfo()));
381
382    assert(mergeTableRegionsMsg.getRegionInfoCount() == 2);
383    regionsToMerge = new RegionInfo[mergeTableRegionsMsg.getRegionInfoCount()];
384    for (int i = 0; i < regionsToMerge.length; i++) {
385      regionsToMerge[i] = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getRegionInfo(i));
386    }
387
388    mergedRegion = ProtobufUtil.toRegionInfo(mergeTableRegionsMsg.getMergedRegionInfo());
389  }
390
391  @Override
392  public void toStringClassDetails(StringBuilder sb) {
393    sb.append(getClass().getSimpleName());
394    sb.append(" table=");
395    sb.append(getTableName());
396    sb.append(", regions=");
397    sb.append(RegionInfo.getShortNameToLog(regionsToMerge));
398    sb.append(", force=");
399    sb.append(force);
400  }
401
402  @Override
403  protected LockState acquireLock(final MasterProcedureEnv env) {
404    if (env.getProcedureScheduler().waitRegions(this, getTableName(),
405        mergedRegion, regionsToMerge[0], regionsToMerge[1])) {
406      try {
407        LOG.debug(LockState.LOCK_EVENT_WAIT + " " + env.getProcedureScheduler().dumpLocks());
408      } catch (IOException e) {
409        // Ignore, just for logging
410      }
411      return LockState.LOCK_EVENT_WAIT;
412    }
413    return LockState.LOCK_ACQUIRED;
414  }
415
416  @Override
417  protected void releaseLock(final MasterProcedureEnv env) {
418    env.getProcedureScheduler().wakeRegions(this, getTableName(),
419      mergedRegion, regionsToMerge[0], regionsToMerge[1]);
420  }
421
422  @Override
423  protected boolean holdLock(MasterProcedureEnv env) {
424    return true;
425  }
426
427  @Override
428  public TableName getTableName() {
429    return mergedRegion.getTable();
430  }
431
432  @Override
433  public TableOperationType getTableOperationType() {
434    return TableOperationType.REGION_MERGE;
435  }
436
437  @Override
438  protected ProcedureMetrics getProcedureMetrics(MasterProcedureEnv env) {
439    return env.getAssignmentManager().getAssignmentManagerMetrics().getMergeProcMetrics();
440  }
441
442  /**
443   * Return list of regions that have recovered.edits... usually its an empty list.
444   * @param env the master env
445   * @throws IOException IOException
446   */
447  private List<RegionInfo> hasRecoveredEdits(final MasterProcedureEnv env) throws IOException {
448    List<RegionInfo> ris =  new ArrayList<RegionInfo>(regionsToMerge.length);
449    for (int i = 0; i < regionsToMerge.length; i++) {
450      RegionInfo ri = regionsToMerge[i];
451      if (SplitTableRegionProcedure.hasRecoveredEdits(env, ri)) {
452        ris.add(ri);
453      }
454    }
455    return ris;
456  }
457
458  /**
459   * Prepare merge and do some check
460   * @param env MasterProcedureEnv
461   * @throws IOException
462   */
463  private boolean prepareMergeRegion(final MasterProcedureEnv env) throws IOException {
464    // Fail if we are taking snapshot for the given table
465    TableName tn = regionsToMerge[0].getTable();
466    if (env.getMasterServices().getSnapshotManager().isTakingSnapshot(tn)) {
467      throw new MergeRegionException("Skip merging regions " +
468          RegionInfo.getShortNameToLog(regionsToMerge) + ", because we are snapshotting " + tn);
469    }
470    if (!env.getMasterServices().isSplitOrMergeEnabled(MasterSwitchType.MERGE)) {
471      String regionsStr = Arrays.deepToString(this.regionsToMerge);
472      LOG.warn("Merge switch is off! skip merge of " + regionsStr);
473      super.setFailure(getClass().getSimpleName(),
474          new IOException("Merge of " + regionsStr + " failed because merge switch is off"));
475      return false;
476    }
477    // See HBASE-21395, for 2.0.x and 2.1.x only.
478    // A safe fence here, if there is a table procedure going on, abort the merge.
479    // There some cases that may lead to table procedure roll back (more serious
480    // than roll back the merge procedure here), or the merged regions was brought online
481    // by the table procedure because of the race between merge procedure and table procedure
482    List<AbstractStateMachineTableProcedure> tableProcedures = env
483        .getMasterServices().getProcedures().stream()
484        .filter(p -> p instanceof AbstractStateMachineTableProcedure)
485        .map(p -> (AbstractStateMachineTableProcedure) p)
486        .filter(p -> p.getProcId() != this.getProcId() && p.getTableName()
487            .equals(regionsToMerge[0].getTable()) && !p.isFinished()
488            && TableQueue.requireTableExclusiveLock(p))
489        .collect(Collectors.toList());
490    if (tableProcedures != null && tableProcedures.size() > 0) {
491      throw new MergeRegionException(tableProcedures.get(0).toString()
492          + " is going on against the same table, abort the merge of " + this
493          .toString());
494    }
495
496    CatalogJanitor catalogJanitor = env.getMasterServices().getCatalogJanitor();
497    RegionStates regionStates = env.getAssignmentManager().getRegionStates();
498    for (RegionInfo ri: this.regionsToMerge) {
499      if (!catalogJanitor.cleanMergeQualifier(ri)) {
500        String msg = "Skip merging " + RegionInfo.getShortNameToLog(regionsToMerge) +
501            ", because parent " + RegionInfo.getShortNameToLog(ri) + " has a merge qualifier";
502        LOG.warn(msg);
503        throw new MergeRegionException(msg);
504      }
505      RegionState state = regionStates.getRegionState(ri.getEncodedName());
506      if (state == null) {
507        throw new UnknownRegionException("No state for " + RegionInfo.getShortNameToLog(ri));
508      }
509      if (!state.isOpened()) {
510        throw new MergeRegionException("Unable to merge regions that are not online: " + ri);
511      }
512      // Ask the remote regionserver if regions are mergeable. If we get an IOE, report it
513      // along with the failure, so we can see why regions are not mergeable at this time.
514      try {
515        if (!isMergeable(env, state)) {
516          return false;
517        }
518      } catch (IOException e) {
519        IOException ioe = new IOException(RegionInfo.getShortNameToLog(ri) + " NOT mergeable", e);
520        super.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      Util.getRegionInfoResponse(env, rs.getServerName(), rs.getRegion());
534    return response.hasMergeable() && response.getMergeable();
535  }
536
537  /**
538   * Pre merge region action
539   * @param env MasterProcedureEnv
540   **/
541  private void preMergeRegions(final MasterProcedureEnv env) throws IOException {
542    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
543    if (cpHost != null) {
544      cpHost.preMergeRegionsAction(regionsToMerge, getUser());
545    }
546    // TODO: Clean up split and merge. Currently all over the place.
547    try {
548      env.getMasterServices().getMasterQuotaManager().onRegionMerged(this.mergedRegion);
549    } catch (QuotaExceededException e) {
550      env.getMasterServices().getRegionNormalizer().planSkipped(this.mergedRegion,
551          NormalizationPlan.PlanType.MERGE);
552      throw e;
553    }
554  }
555
556  /**
557   * Action after rollback a merge table regions action.
558   */
559  private void postRollBackMergeRegions(final MasterProcedureEnv env) throws IOException {
560    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
561    if (cpHost != null) {
562      cpHost.postRollBackMergeRegionsAction(regionsToMerge, getUser());
563    }
564  }
565
566  /**
567   * Set the region states to MERGING state
568   * @param env MasterProcedureEnv
569   */
570  public void setRegionStateToMerging(final MasterProcedureEnv env) {
571    // Set State.MERGING to regions to be merged
572    RegionStates regionStates = env.getAssignmentManager().getRegionStates();
573    for (RegionInfo ri: this.regionsToMerge) {
574      regionStates.getRegionStateNode(ri).setState(State.MERGING);
575    }
576  }
577
578  /**
579   * Create merged region.
580   * The way the merge works is that we make a 'merges' temporary
581   * directory in the FIRST parent region to merge (Do not change this without
582   * also changing the rollback where we look in this FIRST region for the
583   * merge dir). We then collect here references to all the store files in all
584   * the parent regions including those of the FIRST parent region into a
585   * subdirectory, named for the resultant merged region. We then call
586   * commitMergeRegion. It finds this subdirectory of storefile references
587   * and moves them under the new merge region (creating the region layout
588   * as side effect). After assign of the new merge region, we will run a
589   * compaction. This will undo the references but the reference files remain
590   * in place until the archiver runs (which it does on a period as a chore
591   * in the RegionServer that hosts the merge region -- see
592   * CompactedHFilesDischarger). Once the archiver has moved aside the
593   * no-longer used references, the merge region no longer has references.
594   * The catalog janitor will notice when it runs next and it will remove
595   * the old parent regions.
596   */
597  private void createMergedRegion(final MasterProcedureEnv env) throws IOException {
598    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
599    final Path tabledir = FSUtils.getTableDir(mfs.getRootDir(), regionsToMerge[0].getTable());
600    final FileSystem fs = mfs.getFileSystem();
601    HRegionFileSystem mergeRegionFs = null;
602    for (RegionInfo ri: this.regionsToMerge) {
603      HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem(
604          env.getMasterConfiguration(), fs, tabledir, ri, false);
605      if (mergeRegionFs == null) {
606        mergeRegionFs = regionFs;
607        mergeRegionFs.createMergesDir();
608      }
609      mergeStoreFiles(env, regionFs, mergeRegionFs.getMergesDir());
610    }
611    assert mergeRegionFs != null;
612    mergeRegionFs.commitMergedRegion(mergedRegion);
613
614    // Prepare to create merged regions
615    env.getAssignmentManager().getRegionStates().
616        getOrCreateRegionStateNode(mergedRegion).setState(State.MERGING_NEW);
617  }
618
619  /**
620   * Create reference file(s) to parent region hfiles in the <code>mergeDir</code>
621   * @param regionFs merge parent region file system
622   * @param mergeDir the temp directory in which we are accumulating references.
623   */
624  private void mergeStoreFiles(final MasterProcedureEnv env, final HRegionFileSystem regionFs,
625      final Path mergeDir) throws IOException {
626    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
627    final Configuration conf = env.getMasterConfiguration();
628    final TableDescriptor htd = env.getMasterServices().getTableDescriptors().get(getTableName());
629    for (ColumnFamilyDescriptor hcd : htd.getColumnFamilies()) {
630      String family = hcd.getNameAsString();
631      final Collection<StoreFileInfo> storeFiles = regionFs.getStoreFiles(family);
632      if (storeFiles != null && storeFiles.size() > 0) {
633        for (StoreFileInfo storeFileInfo : storeFiles) {
634          // Create reference file(s) to parent region file here in mergedDir.
635          // As this procedure is running on master, use CacheConfig.DISABLED means
636          // don't cache any block.
637          regionFs.mergeStoreFile(mergedRegion, family, new HStoreFile(mfs.getFileSystem(),
638              storeFileInfo, conf, CacheConfig.DISABLED, hcd.getBloomFilterType(), true),
639            mergeDir);
640        }
641      }
642    }
643  }
644
645  /**
646   * Clean up a merged region on rollback after failure.
647   */
648  private void cleanupMergedRegion(final MasterProcedureEnv env) throws IOException {
649    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
650    TableName tn = this.regionsToMerge[0].getTable();
651    final Path tabledir = FSUtils.getTableDir(mfs.getRootDir(), tn);
652    final FileSystem fs = mfs.getFileSystem();
653    // See createMergedRegion above where we specify the merge dir as being in the
654    // FIRST merge parent region.
655    HRegionFileSystem regionFs = HRegionFileSystem.openRegionFromFileSystem(
656      env.getMasterConfiguration(), fs, tabledir, regionsToMerge[0], false);
657    regionFs.cleanupMergedRegion(mergedRegion);
658  }
659
660  /**
661   * Rollback close regions
662   * @param env MasterProcedureEnv
663   **/
664  private void rollbackCloseRegionsForMerge(final MasterProcedureEnv env) throws IOException {
665    // Check whether the region is closed; if so, open it in the same server
666    final int regionReplication = getRegionReplication(env);
667    final ServerName serverName = getServerName(env);
668
669    final AssignProcedure[] procs =
670        new AssignProcedure[regionsToMerge.length * regionReplication];
671    int procsIdx = 0;
672    for (int i = 0; i < regionsToMerge.length; ++i) {
673      for (int j = 0; j < regionReplication; ++j) {
674        final RegionInfo hri = RegionReplicaUtil.getRegionInfoForReplica(regionsToMerge[i], j);
675        procs[procsIdx++] = env.getAssignmentManager().createAssignProcedure(hri, serverName);
676      }
677    }
678    env.getMasterServices().getMasterProcedureExecutor().submitProcedures(procs);
679  }
680
681  private UnassignProcedure[] createUnassignProcedures(final MasterProcedureEnv env,
682      final int regionReplication) {
683    final UnassignProcedure[] procs =
684        new UnassignProcedure[regionsToMerge.length * regionReplication];
685    int procsIdx = 0;
686    for (int i = 0; i < regionsToMerge.length; ++i) {
687      for (int j = 0; j < regionReplication; ++j) {
688        final RegionInfo hri = RegionReplicaUtil.getRegionInfoForReplica(regionsToMerge[i], j);
689        procs[procsIdx++] = env.getAssignmentManager().
690            createUnassignProcedure(hri, null, true, !RegionReplicaUtil.isDefaultReplica(hri));
691      }
692    }
693    return procs;
694  }
695
696  private AssignProcedure[] createAssignProcedures(final MasterProcedureEnv env,
697      final int regionReplication) {
698    final ServerName targetServer = getServerName(env);
699    final AssignProcedure[] procs = new AssignProcedure[regionReplication];
700    for (int i = 0; i < procs.length; ++i) {
701      final RegionInfo hri = RegionReplicaUtil.getRegionInfoForReplica(mergedRegion, i);
702      procs[i] = env.getAssignmentManager().createAssignProcedure(hri, targetServer);
703    }
704    return procs;
705  }
706
707  private int getRegionReplication(final MasterProcedureEnv env) throws IOException {
708    return env.getMasterServices().getTableDescriptors().get(getTableName()).
709        getRegionReplication();
710  }
711
712  /**
713   * Post merge region action
714   * @param env MasterProcedureEnv
715   **/
716  private void preMergeRegionsCommit(final MasterProcedureEnv env) throws IOException {
717    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
718    if (cpHost != null) {
719      @MetaMutationAnnotation
720      final List<Mutation> metaEntries = new ArrayList<>();
721      cpHost.preMergeRegionsCommit(regionsToMerge, metaEntries, getUser());
722      try {
723        for (Mutation p : metaEntries) {
724          RegionInfo.parseRegionName(p.getRow());
725        }
726      } catch (IOException e) {
727        LOG.error("Row key of mutation from coprocessor is not parsable as region name. "
728          + "Mutations from coprocessor should only be for hbase:meta table.", 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   * @return serverName
769   */
770  private ServerName getServerName(final MasterProcedureEnv env) {
771    if (regionLocation == null) {
772      regionLocation = env.getAssignmentManager().getRegionStates().
773          getRegionServerOfRegion(regionsToMerge[0]);
774      // May still be null here but return null and let caller deal.
775      // Means we lost the in-memory-only location. We are in recovery
776      // or so. The caller should be able to deal w/ a null ServerName.
777      // Let them go to the Balancer to find one to use instead.
778    }
779    return regionLocation;
780  }
781
782  private void writeMaxSequenceIdFile(MasterProcedureEnv env) throws IOException {
783    MasterFileSystem fs = env.getMasterFileSystem();
784    long maxSequenceId = -1L;
785    for (RegionInfo region : regionsToMerge) {
786      maxSequenceId =
787          Math.max(maxSequenceId, WALSplitter.getMaxRegionSequenceId(env.getMasterConfiguration(),
788            region, fs::getFileSystem, fs::getWALFileSystem));
789    }
790    if (maxSequenceId > 0) {
791      WALSplitter.writeRegionSequenceIdFile(fs.getWALFileSystem(),
792        getWALRegionDir(env, mergedRegion), maxSequenceId);
793    }
794  }
795
796  /**
797   * @return The merged region. Maybe be null if called to early or we failed.
798   */
799  @VisibleForTesting
800  RegionInfo getMergedRegion() {
801    return this.mergedRegion;
802  }
803
804  @Override
805  protected boolean abort(MasterProcedureEnv env) {
806    // Abort means rollback. We can't rollback all steps. HBASE-18018 added abort to all
807    // Procedures. Here is a Procedure that has a PONR and cannot be aborted once it enters this
808    // range of steps; what do we do for these should an operator want to cancel them? HBASE-20022.
809    return isRollbackSupported(getCurrentState()) && super.abort(env);
810  }
811}