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.procedure;
019
020import java.io.IOException;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026import java.util.function.Supplier;
027import java.util.stream.Collectors;
028import java.util.stream.IntStream;
029import org.apache.hadoop.hbase.ConcurrentTableModificationException;
030import org.apache.hadoop.hbase.DoNotRetryIOException;
031import org.apache.hadoop.hbase.HBaseIOException;
032import org.apache.hadoop.hbase.HConstants;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.TableNotFoundException;
035import org.apache.hadoop.hbase.client.RegionInfo;
036import org.apache.hadoop.hbase.client.RegionReplicaUtil;
037import org.apache.hadoop.hbase.client.TableDescriptor;
038import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
039import org.apache.hadoop.hbase.fs.ErasureCodingUtils;
040import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
041import org.apache.hadoop.hbase.master.zksyncer.MetaLocationSyncer;
042import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
043import org.apache.hadoop.hbase.regionserver.compactions.CustomCellTieredUtils;
044import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerValidationUtils;
045import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
046import org.apache.hadoop.hbase.util.Bytes;
047import org.apache.yetus.audience.InterfaceAudience;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
052import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
053import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.ModifyTableState;
054
055@InterfaceAudience.Private
056public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<ModifyTableState> {
057  private static final Logger LOG = LoggerFactory.getLogger(ModifyTableProcedure.class);
058
059  private TableDescriptor unmodifiedTableDescriptor = null;
060  private TableDescriptor modifiedTableDescriptor;
061  private boolean deleteColumnFamilyInModify;
062  private boolean shouldCheckDescriptor;
063  private boolean reopenRegions;
064  private String recoverySnapshotName;
065
066  /**
067   * List of column families that cannot be deleted from the hbase:meta table. They are critical to
068   * cluster operation. This is a bit of an odd place to keep this list but then this is the tooling
069   * that does add/remove. Keeping it local!
070   */
071  private static final List<byte[]> UNDELETABLE_META_COLUMNFAMILIES =
072    Collections.unmodifiableList(Arrays.asList(HConstants.CATALOG_FAMILY, HConstants.TABLE_FAMILY,
073      HConstants.REPLICATION_BARRIER_FAMILY, HConstants.NAMESPACE_FAMILY));
074
075  public ModifyTableProcedure() {
076    super();
077    initialize(null, false);
078  }
079
080  public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd)
081    throws HBaseIOException {
082    this(env, htd, null);
083  }
084
085  public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd,
086    final ProcedurePrepareLatch latch) throws HBaseIOException {
087    this(env, htd, latch, null, false, true);
088  }
089
090  public ModifyTableProcedure(final MasterProcedureEnv env,
091    final TableDescriptor newTableDescriptor, final ProcedurePrepareLatch latch,
092    final TableDescriptor oldTableDescriptor, final boolean shouldCheckDescriptor,
093    final boolean reopenRegions) throws HBaseIOException {
094    super(env, latch);
095    this.reopenRegions = reopenRegions;
096    initialize(oldTableDescriptor, shouldCheckDescriptor);
097    this.modifiedTableDescriptor = newTableDescriptor;
098    preflightChecks(env, null/* No table checks; if changing peers, table can be online */);
099  }
100
101  @Override
102  protected void preflightChecks(MasterProcedureEnv env, Boolean enabled) throws HBaseIOException {
103    super.preflightChecks(env, enabled);
104    if (this.modifiedTableDescriptor.isMetaTable()) {
105      // If we are modifying the hbase:meta table, make sure we are not deleting critical
106      // column families else we'll damage the cluster.
107      Set<byte[]> cfs = this.modifiedTableDescriptor.getColumnFamilyNames();
108      for (byte[] family : UNDELETABLE_META_COLUMNFAMILIES) {
109        if (!cfs.contains(family)) {
110          throw new HBaseIOException(
111            "Delete of hbase:meta column family " + Bytes.toString(family));
112        }
113      }
114    }
115
116    if (!reopenRegions) {
117      if (this.unmodifiedTableDescriptor == null) {
118        throw new HBaseIOException(
119          "unmodifiedTableDescriptor cannot be null when this table modification won't reopen regions");
120      }
121      if (
122        !this.unmodifiedTableDescriptor.getTableName()
123          .equals(this.modifiedTableDescriptor.getTableName())
124      ) {
125        throw new HBaseIOException(
126          "Cannot change the table name when this modification won't " + "reopen regions.");
127      }
128      if (
129        this.unmodifiedTableDescriptor.getColumnFamilyCount()
130            != this.modifiedTableDescriptor.getColumnFamilyCount()
131      ) {
132        throw new HBaseIOException(
133          "Cannot add or remove column families when this modification " + "won't reopen regions.");
134      }
135      if (
136        this.unmodifiedTableDescriptor.getCoprocessorDescriptors().hashCode()
137            != this.modifiedTableDescriptor.getCoprocessorDescriptors().hashCode()
138      ) {
139        throw new HBaseIOException(
140          "Can not modify Coprocessor when table modification won't reopen regions");
141      }
142      final Set<String> s = new HashSet<>(Arrays.asList(TableDescriptorBuilder.REGION_REPLICATION,
143        TableDescriptorBuilder.REGION_MEMSTORE_REPLICATION, RSGroupInfo.TABLE_DESC_PROP_GROUP));
144      for (String k : s) {
145        if (
146          isTablePropertyModified(this.unmodifiedTableDescriptor, this.modifiedTableDescriptor, k)
147        ) {
148          throw new HBaseIOException(
149            "Can not modify " + k + " of a table when modification won't reopen regions");
150        }
151      }
152    }
153  }
154
155  /**
156   * Comparing the value associated with a given key across two TableDescriptor instances'
157   * properties.
158   * @return True if the table property <code>key</code> is the same in both.
159   */
160  private boolean isTablePropertyModified(TableDescriptor oldDescriptor,
161    TableDescriptor newDescriptor, String key) {
162    String oldV = oldDescriptor.getValue(key);
163    String newV = newDescriptor.getValue(key);
164    if (oldV == null && newV == null) {
165      return false;
166    } else if (oldV != null && newV != null && oldV.equals(newV)) {
167      return false;
168    }
169    return true;
170  }
171
172  private void initialize(final TableDescriptor unmodifiedTableDescriptor,
173    final boolean shouldCheckDescriptor) {
174    this.unmodifiedTableDescriptor = unmodifiedTableDescriptor;
175    this.shouldCheckDescriptor = shouldCheckDescriptor;
176    this.deleteColumnFamilyInModify = false;
177  }
178
179  @Override
180  protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state)
181    throws InterruptedException {
182    LOG.trace("{} execute state={}", this, state);
183    try {
184      switch (state) {
185        case MODIFY_TABLE_PREPARE:
186          prepareModify(env);
187          setNextState(ModifyTableState.MODIFY_TABLE_PRE_OPERATION);
188          break;
189        case MODIFY_TABLE_PRE_OPERATION:
190          preModify(env, state);
191          // We cannot allow changes to region replicas when 'reopenRegions==false',
192          // as this mode bypasses the state management required for modifying region replicas.
193          if (reopenRegions) {
194            // Check if we should create a recovery snapshot for column family deletion
195            if (deleteColumnFamilyInModify && RecoverySnapshotUtils.isRecoveryEnabled(env)) {
196              setNextState(ModifyTableState.MODIFY_TABLE_SNAPSHOT);
197            } else {
198              setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS);
199            }
200          } else {
201            setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR);
202          }
203          break;
204        case MODIFY_TABLE_SNAPSHOT:
205          // Create recovery snapshot procedure as child procedure
206          recoverySnapshotName = RecoverySnapshotUtils.generateSnapshotName(getTableName());
207          SnapshotProcedure snapshotProcedure = RecoverySnapshotUtils.createSnapshotProcedure(env,
208            getTableName(), recoverySnapshotName, unmodifiedTableDescriptor);
209          // Submit snapshot procedure as child procedure
210          addChildProcedure(snapshotProcedure);
211          LOG.debug("Creating recovery snapshot {} for table {} before column deletion",
212            recoverySnapshotName, getTableName());
213          setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS);
214          break;
215        case MODIFY_TABLE_CLOSE_EXCESS_REPLICAS:
216          if (isTableEnabled(env)) {
217            closeExcessReplicasIfNeeded(env);
218          }
219          setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR);
220          break;
221        case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
222          updateTableDescriptor(env);
223          if (reopenRegions) {
224            setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN);
225          } else {
226            setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
227          }
228          break;
229        case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
230          removeReplicaColumnsIfNeeded(env);
231          setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
232          break;
233        case MODIFY_TABLE_POST_OPERATION:
234          postModify(env, state);
235          if (reopenRegions) {
236            setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS);
237          } else
238            if (ErasureCodingUtils.needsSync(unmodifiedTableDescriptor, modifiedTableDescriptor)) {
239              setNextState(ModifyTableState.MODIFY_TABLE_SYNC_ERASURE_CODING_POLICY);
240            } else {
241              return Flow.NO_MORE_STATE;
242            }
243          break;
244        case MODIFY_TABLE_REOPEN_ALL_REGIONS:
245          if (isTableEnabled(env)) {
246            addChildProcedure(ReopenTableRegionsProcedure.throttled(env.getMasterConfiguration(),
247              env.getMasterServices().getTableDescriptors().get(getTableName())));
248          }
249          setNextState(ModifyTableState.MODIFY_TABLE_ASSIGN_NEW_REPLICAS);
250          break;
251        case MODIFY_TABLE_ASSIGN_NEW_REPLICAS:
252          assignNewReplicasIfNeeded(env);
253          if (TableName.isMetaTableName(getTableName())) {
254            MetaLocationSyncer syncer = env.getMasterServices().getMetaLocationSyncer();
255            if (syncer != null) {
256              syncer.setMetaReplicaCount(modifiedTableDescriptor.getRegionReplication());
257            }
258          }
259          if (deleteColumnFamilyInModify) {
260            setNextState(ModifyTableState.MODIFY_TABLE_DELETE_FS_LAYOUT);
261          } else
262            if (ErasureCodingUtils.needsSync(unmodifiedTableDescriptor, modifiedTableDescriptor)) {
263              setNextState(ModifyTableState.MODIFY_TABLE_SYNC_ERASURE_CODING_POLICY);
264            } else {
265              return Flow.NO_MORE_STATE;
266            }
267          break;
268        case MODIFY_TABLE_DELETE_FS_LAYOUT:
269          deleteFromFs(env, unmodifiedTableDescriptor, modifiedTableDescriptor);
270          if (ErasureCodingUtils.needsSync(unmodifiedTableDescriptor, modifiedTableDescriptor)) {
271            setNextState(ModifyTableState.MODIFY_TABLE_SYNC_ERASURE_CODING_POLICY);
272            break;
273          } else {
274            return Flow.NO_MORE_STATE;
275          }
276        case MODIFY_TABLE_SYNC_ERASURE_CODING_POLICY:
277          ErasureCodingUtils.sync(env.getMasterFileSystem().getFileSystem(),
278            env.getMasterFileSystem().getRootDir(), modifiedTableDescriptor);
279          return Flow.NO_MORE_STATE;
280        default:
281          throw new UnsupportedOperationException("unhandled state=" + state);
282      }
283    } catch (IOException e) {
284      if (isRollbackSupported(state)) {
285        setFailure("master-modify-table", e);
286      } else {
287        LOG.warn("Retriable error trying to modify table={} (in state={})", getTableName(), state,
288          e);
289      }
290    }
291    return Flow.HAS_MORE_STATE;
292  }
293
294  @Override
295  protected void rollbackState(final MasterProcedureEnv env, final ModifyTableState state)
296    throws IOException {
297    switch (state) {
298      case MODIFY_TABLE_PREPARE:
299      case MODIFY_TABLE_PRE_OPERATION:
300        // Nothing to roll back.
301        // TODO: Coprocessor rollback semantic is still undefined.
302        break;
303      case MODIFY_TABLE_SNAPSHOT:
304        // Handle recovery snapshot rollback. There is no DeleteSnapshotProcedure as such to use
305        // here directly as a child procedure, so we call a utility method to delete the snapshot
306        // which uses the SnapshotManager to delete the snapshot.
307        if (recoverySnapshotName != null) {
308          RecoverySnapshotUtils.deleteRecoverySnapshot(env, recoverySnapshotName, getTableName());
309          recoverySnapshotName = null;
310        }
311        break;
312      default:
313        // Modify from other states doesn't have a rollback. The execution will succeed, at some
314        // point.
315        throw new UnsupportedOperationException("unhandled state=" + state);
316    }
317  }
318
319  @Override
320  protected boolean isRollbackSupported(final ModifyTableState state) {
321    switch (state) {
322      case MODIFY_TABLE_PREPARE:
323      case MODIFY_TABLE_PRE_OPERATION:
324      case MODIFY_TABLE_SNAPSHOT:
325      case MODIFY_TABLE_CLOSE_EXCESS_REPLICAS:
326        return true;
327      default:
328        return false;
329    }
330  }
331
332  @Override
333  protected void completionCleanup(final MasterProcedureEnv env) {
334    releaseSyncLatch();
335  }
336
337  @Override
338  protected ModifyTableState getState(final int stateId) {
339    return ModifyTableState.forNumber(stateId);
340  }
341
342  @Override
343  protected int getStateId(final ModifyTableState state) {
344    return state.getNumber();
345  }
346
347  @Override
348  protected ModifyTableState getInitialState() {
349    return ModifyTableState.MODIFY_TABLE_PREPARE;
350  }
351
352  @Override
353  protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException {
354    super.serializeStateData(serializer);
355
356    MasterProcedureProtos.ModifyTableStateData.Builder modifyTableMsg =
357      MasterProcedureProtos.ModifyTableStateData.newBuilder()
358        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
359        .setModifiedTableSchema(ProtobufUtil.toTableSchema(modifiedTableDescriptor))
360        .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify)
361        .setShouldCheckDescriptor(shouldCheckDescriptor).setReopenRegions(reopenRegions);
362
363    if (unmodifiedTableDescriptor != null) {
364      modifyTableMsg
365        .setUnmodifiedTableSchema(ProtobufUtil.toTableSchema(unmodifiedTableDescriptor));
366    }
367
368    if (recoverySnapshotName != null) {
369      modifyTableMsg.setSnapshotName(recoverySnapshotName);
370    }
371
372    serializer.serialize(modifyTableMsg.build());
373  }
374
375  @Override
376  protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException {
377    super.deserializeStateData(serializer);
378
379    MasterProcedureProtos.ModifyTableStateData modifyTableMsg =
380      serializer.deserialize(MasterProcedureProtos.ModifyTableStateData.class);
381    setUser(MasterProcedureUtil.toUserInfo(modifyTableMsg.getUserInfo()));
382    modifiedTableDescriptor =
383      ProtobufUtil.toTableDescriptor(modifyTableMsg.getModifiedTableSchema());
384    deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify();
385    shouldCheckDescriptor =
386      modifyTableMsg.hasShouldCheckDescriptor() ? modifyTableMsg.getShouldCheckDescriptor() : false;
387    reopenRegions = modifyTableMsg.hasReopenRegions() ? modifyTableMsg.getReopenRegions() : true;
388
389    if (modifyTableMsg.hasUnmodifiedTableSchema()) {
390      unmodifiedTableDescriptor =
391        ProtobufUtil.toTableDescriptor(modifyTableMsg.getUnmodifiedTableSchema());
392    }
393
394    if (modifyTableMsg.hasSnapshotName()) {
395      recoverySnapshotName = modifyTableMsg.getSnapshotName();
396    }
397  }
398
399  @Override
400  public TableName getTableName() {
401    return modifiedTableDescriptor.getTableName();
402  }
403
404  @Override
405  public TableOperationType getTableOperationType() {
406    return TableOperationType.EDIT;
407  }
408
409  /**
410   * Check conditions before any real action of modifying a table.
411   */
412  private void prepareModify(final MasterProcedureEnv env) throws IOException {
413    // Checks whether the table exists
414    if (!env.getMasterServices().getTableDescriptors().exists(getTableName())) {
415      throw new TableNotFoundException(getTableName());
416    }
417
418    // check that we have at least 1 CF
419    if (modifiedTableDescriptor.getColumnFamilyCount() == 0) {
420      throw new DoNotRetryIOException(
421        "Table " + getTableName().toString() + " should have at least one column family.");
422    }
423
424    // If descriptor check is enabled, check whether the table descriptor when procedure was
425    // submitted matches with the current
426    // table descriptor of the table, else retrieve the old descriptor
427    // for comparison in order to update the descriptor.
428    if (shouldCheckDescriptor) {
429      if (
430        TableDescriptor.COMPARATOR.compare(unmodifiedTableDescriptor,
431          env.getMasterServices().getTableDescriptors().get(getTableName())) != 0
432      ) {
433        LOG.error("Error while modifying table '" + getTableName().toString()
434          + "' Skipping procedure : " + this);
435        throw new ConcurrentTableModificationException(
436          "Skipping modify table operation on table '" + getTableName().toString()
437            + "' as it has already been modified by some other concurrent operation, "
438            + "Please retry.");
439      }
440    } else {
441      this.unmodifiedTableDescriptor =
442        env.getMasterServices().getTableDescriptors().get(getTableName());
443    }
444
445    this.deleteColumnFamilyInModify =
446      isDeleteColumnFamily(unmodifiedTableDescriptor, modifiedTableDescriptor);
447    if (
448      !unmodifiedTableDescriptor.getRegionServerGroup()
449        .equals(modifiedTableDescriptor.getRegionServerGroup())
450    ) {
451      Supplier<String> forWhom = () -> "table " + getTableName();
452      RSGroupInfo rsGroupInfo = MasterProcedureUtil.checkGroupExists(
453        env.getMasterServices().getRSGroupInfoManager()::getRSGroup,
454        modifiedTableDescriptor.getRegionServerGroup(), forWhom);
455      MasterProcedureUtil.checkGroupNotEmpty(rsGroupInfo, forWhom);
456    }
457
458    // check for store file tracker configurations
459    StoreFileTrackerValidationUtils.checkForModifyTable(env.getMasterConfiguration(),
460      unmodifiedTableDescriptor, modifiedTableDescriptor, !isTableEnabled(env));
461    CustomCellTieredUtils.checkForModifyTable(modifiedTableDescriptor);
462  }
463
464  /**
465   * Find out whether all column families in unmodifiedTableDescriptor also exists in the
466   * modifiedTableDescriptor.
467   * @return True if we are deleting a column family.
468   */
469  private static boolean isDeleteColumnFamily(TableDescriptor originalDescriptor,
470    TableDescriptor newDescriptor) {
471    boolean result = false;
472    final Set<byte[]> originalFamilies = originalDescriptor.getColumnFamilyNames();
473    final Set<byte[]> newFamilies = newDescriptor.getColumnFamilyNames();
474    for (byte[] familyName : originalFamilies) {
475      if (!newFamilies.contains(familyName)) {
476        result = true;
477        break;
478      }
479    }
480    return result;
481  }
482
483  /**
484   * Action before modifying table.
485   * @param env   MasterProcedureEnv
486   * @param state the procedure state
487   */
488  private void preModify(final MasterProcedureEnv env, final ModifyTableState state)
489    throws IOException, InterruptedException {
490    runCoprocessorAction(env, state);
491  }
492
493  /**
494   * Update descriptor
495   * @param env MasterProcedureEnv
496   **/
497  private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
498    env.getMasterServices().getTableDescriptors().update(modifiedTableDescriptor);
499  }
500
501  /**
502   * Removes from hdfs the families that are not longer present in the new table descriptor.
503   * @param env MasterProcedureEnv
504   */
505  private void deleteFromFs(final MasterProcedureEnv env, final TableDescriptor oldTableDescriptor,
506    final TableDescriptor newTableDescriptor) throws IOException {
507    final Set<byte[]> oldFamilies = oldTableDescriptor.getColumnFamilyNames();
508    final Set<byte[]> newFamilies = newTableDescriptor.getColumnFamilyNames();
509    for (byte[] familyName : oldFamilies) {
510      if (!newFamilies.contains(familyName)) {
511        MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(env, getTableName(),
512          getRegionInfoList(env), familyName,
513          oldTableDescriptor.getColumnFamily(familyName).isMobEnabled());
514      }
515    }
516  }
517
518  /**
519   * remove replica columns if necessary.
520   */
521  private void removeReplicaColumnsIfNeeded(MasterProcedureEnv env) throws IOException {
522    final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication();
523    final int newReplicaCount = modifiedTableDescriptor.getRegionReplication();
524    if (newReplicaCount >= oldReplicaCount) {
525      return;
526    }
527    env.getAssignmentManager().getRegionStateStore().removeRegionReplicas(getTableName(),
528      oldReplicaCount, newReplicaCount);
529    env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()).stream()
530      .filter(r -> r.getReplicaId() >= newReplicaCount)
531      .forEach(env.getAssignmentManager().getRegionStates()::deleteRegion);
532  }
533
534  private void assignNewReplicasIfNeeded(MasterProcedureEnv env) throws IOException {
535    final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication();
536    final int newReplicaCount = modifiedTableDescriptor.getRegionReplication();
537    if (newReplicaCount <= oldReplicaCount) {
538      return;
539    }
540    if (isTableEnabled(env)) {
541      List<RegionInfo> newReplicas = env.getAssignmentManager().getRegionStates()
542        .getRegionsOfTable(getTableName()).stream().filter(RegionReplicaUtil::isDefaultReplica)
543        .flatMap(primaryRegion -> IntStream.range(oldReplicaCount, newReplicaCount).mapToObj(
544          replicaId -> RegionReplicaUtil.getRegionInfoForReplica(primaryRegion, replicaId)))
545        .collect(Collectors.toList());
546      addChildProcedure(env.getAssignmentManager().createAssignProcedures(newReplicas));
547    }
548  }
549
550  private void closeExcessReplicasIfNeeded(MasterProcedureEnv env) {
551    final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication();
552    final int newReplicaCount = modifiedTableDescriptor.getRegionReplication();
553    if (newReplicaCount >= oldReplicaCount) {
554      return;
555    }
556    addChildProcedure(new CloseExcessRegionReplicasProcedure(getTableName(), newReplicaCount));
557  }
558
559  /**
560   * Action after modifying table.
561   * @param env   MasterProcedureEnv
562   * @param state the procedure state
563   */
564  private void postModify(final MasterProcedureEnv env, final ModifyTableState state)
565    throws IOException, InterruptedException {
566    runCoprocessorAction(env, state);
567  }
568
569  /**
570   * Coprocessor Action.
571   * @param env   MasterProcedureEnv
572   * @param state the procedure state
573   */
574  private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state)
575    throws IOException, InterruptedException {
576    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
577    if (cpHost != null) {
578      switch (state) {
579        case MODIFY_TABLE_PRE_OPERATION:
580          cpHost.preModifyTableAction(getTableName(), unmodifiedTableDescriptor,
581            modifiedTableDescriptor, getUser());
582          break;
583        case MODIFY_TABLE_POST_OPERATION:
584          cpHost.postCompletedModifyTableAction(getTableName(), unmodifiedTableDescriptor,
585            modifiedTableDescriptor, getUser());
586          break;
587        default:
588          throw new UnsupportedOperationException(this + " unhandled state=" + state);
589      }
590    }
591  }
592
593  /**
594   * Fetches all Regions for a table. Cache the result of this method if you need to use it multiple
595   * times. Be aware that it may change over in between calls to this procedure.
596   */
597  private List<RegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
598    return env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName());
599  }
600}