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