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.List;
024import java.util.Set;
025import java.util.function.Supplier;
026import java.util.stream.Collectors;
027import java.util.stream.IntStream;
028import org.apache.hadoop.hbase.ConcurrentTableModificationException;
029import org.apache.hadoop.hbase.DoNotRetryIOException;
030import org.apache.hadoop.hbase.HBaseIOException;
031import org.apache.hadoop.hbase.HConstants;
032import org.apache.hadoop.hbase.TableName;
033import org.apache.hadoop.hbase.TableNotFoundException;
034import org.apache.hadoop.hbase.client.RegionInfo;
035import org.apache.hadoop.hbase.client.RegionReplicaUtil;
036import org.apache.hadoop.hbase.client.TableDescriptor;
037import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
038import org.apache.hadoop.hbase.master.zksyncer.MetaLocationSyncer;
039import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
040import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerValidationUtils;
041import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
042import org.apache.hadoop.hbase.util.Bytes;
043import org.apache.yetus.audience.InterfaceAudience;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
048import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
049import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.ModifyTableState;
050
051@InterfaceAudience.Private
052public class ModifyTableProcedure extends AbstractStateMachineTableProcedure<ModifyTableState> {
053  private static final Logger LOG = LoggerFactory.getLogger(ModifyTableProcedure.class);
054
055  private TableDescriptor unmodifiedTableDescriptor = null;
056  private TableDescriptor modifiedTableDescriptor;
057  private boolean deleteColumnFamilyInModify;
058  private boolean shouldCheckDescriptor;
059  /**
060   * List of column families that cannot be deleted from the hbase:meta table. They are critical to
061   * cluster operation. This is a bit of an odd place to keep this list but then this is the tooling
062   * that does add/remove. Keeping it local!
063   */
064  private static final List<byte[]> UNDELETABLE_META_COLUMNFAMILIES =
065    Collections.unmodifiableList(Arrays.asList(HConstants.CATALOG_FAMILY, HConstants.TABLE_FAMILY,
066      HConstants.REPLICATION_BARRIER_FAMILY));
067
068  public ModifyTableProcedure() {
069    super();
070    initialize(null, false);
071  }
072
073  public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd)
074    throws HBaseIOException {
075    this(env, htd, null);
076  }
077
078  public ModifyTableProcedure(final MasterProcedureEnv env, final TableDescriptor htd,
079    final ProcedurePrepareLatch latch) throws HBaseIOException {
080    this(env, htd, latch, null, false);
081  }
082
083  public ModifyTableProcedure(final MasterProcedureEnv env,
084    final TableDescriptor newTableDescriptor, final ProcedurePrepareLatch latch,
085    final TableDescriptor oldTableDescriptor, final boolean shouldCheckDescriptor)
086    throws HBaseIOException {
087    super(env, latch);
088    initialize(oldTableDescriptor, shouldCheckDescriptor);
089    this.modifiedTableDescriptor = newTableDescriptor;
090    preflightChecks(env, null/* No table checks; if changing peers, table can be online */);
091  }
092
093  @Override
094  protected void preflightChecks(MasterProcedureEnv env, Boolean enabled) throws HBaseIOException {
095    super.preflightChecks(env, enabled);
096    if (this.modifiedTableDescriptor.isMetaTable()) {
097      // If we are modifying the hbase:meta table, make sure we are not deleting critical
098      // column families else we'll damage the cluster.
099      Set<byte[]> cfs = this.modifiedTableDescriptor.getColumnFamilyNames();
100      for (byte[] family : UNDELETABLE_META_COLUMNFAMILIES) {
101        if (!cfs.contains(family)) {
102          throw new HBaseIOException(
103            "Delete of hbase:meta column family " + Bytes.toString(family));
104        }
105      }
106    }
107  }
108
109  private void initialize(final TableDescriptor unmodifiedTableDescriptor,
110    final boolean shouldCheckDescriptor) {
111    this.unmodifiedTableDescriptor = unmodifiedTableDescriptor;
112    this.shouldCheckDescriptor = shouldCheckDescriptor;
113    this.deleteColumnFamilyInModify = false;
114  }
115
116  @Override
117  protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state)
118    throws InterruptedException {
119    LOG.trace("{} execute state={}", this, state);
120    try {
121      switch (state) {
122        case MODIFY_TABLE_PREPARE:
123          prepareModify(env);
124          setNextState(ModifyTableState.MODIFY_TABLE_PRE_OPERATION);
125          break;
126        case MODIFY_TABLE_PRE_OPERATION:
127          preModify(env, state);
128          setNextState(ModifyTableState.MODIFY_TABLE_CLOSE_EXCESS_REPLICAS);
129          break;
130        case MODIFY_TABLE_CLOSE_EXCESS_REPLICAS:
131          if (isTableEnabled(env)) {
132            closeExcessReplicasIfNeeded(env);
133          }
134          setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR);
135          break;
136        case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
137          updateTableDescriptor(env);
138          setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN);
139          break;
140        case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
141          removeReplicaColumnsIfNeeded(env);
142          setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
143          break;
144        case MODIFY_TABLE_POST_OPERATION:
145          postModify(env, state);
146          setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS);
147          break;
148        case MODIFY_TABLE_REOPEN_ALL_REGIONS:
149          if (isTableEnabled(env)) {
150            addChildProcedure(new ReopenTableRegionsProcedure(getTableName()));
151          }
152          setNextState(ModifyTableState.MODIFY_TABLE_ASSIGN_NEW_REPLICAS);
153          break;
154        case MODIFY_TABLE_ASSIGN_NEW_REPLICAS:
155          assignNewReplicasIfNeeded(env);
156          if (TableName.isMetaTableName(getTableName())) {
157            MetaLocationSyncer syncer = env.getMasterServices().getMetaLocationSyncer();
158            if (syncer != null) {
159              syncer.setMetaReplicaCount(modifiedTableDescriptor.getRegionReplication());
160            }
161          }
162          if (deleteColumnFamilyInModify) {
163            setNextState(ModifyTableState.MODIFY_TABLE_DELETE_FS_LAYOUT);
164          } else {
165            return Flow.NO_MORE_STATE;
166          }
167          break;
168        case MODIFY_TABLE_DELETE_FS_LAYOUT:
169          deleteFromFs(env, unmodifiedTableDescriptor, modifiedTableDescriptor);
170          return Flow.NO_MORE_STATE;
171        default:
172          throw new UnsupportedOperationException("unhandled state=" + state);
173      }
174    } catch (IOException e) {
175      if (isRollbackSupported(state)) {
176        setFailure("master-modify-table", e);
177      } else {
178        LOG.warn("Retriable error trying to modify table={} (in state={})", getTableName(), state,
179          e);
180      }
181    }
182    return Flow.HAS_MORE_STATE;
183  }
184
185  @Override
186  protected void rollbackState(final MasterProcedureEnv env, final ModifyTableState state)
187    throws IOException {
188    if (
189      state == ModifyTableState.MODIFY_TABLE_PREPARE
190        || state == ModifyTableState.MODIFY_TABLE_PRE_OPERATION
191    ) {
192      // nothing to rollback, pre-modify is just checks.
193      // TODO: coprocessor rollback semantic is still undefined.
194      return;
195    }
196
197    // The delete doesn't have a rollback. The execution will succeed, at some point.
198    throw new UnsupportedOperationException("unhandled state=" + state);
199  }
200
201  @Override
202  protected boolean isRollbackSupported(final ModifyTableState state) {
203    switch (state) {
204      case MODIFY_TABLE_PRE_OPERATION:
205      case MODIFY_TABLE_PREPARE:
206        return true;
207      default:
208        return false;
209    }
210  }
211
212  @Override
213  protected void completionCleanup(final MasterProcedureEnv env) {
214    releaseSyncLatch();
215  }
216
217  @Override
218  protected ModifyTableState getState(final int stateId) {
219    return ModifyTableState.forNumber(stateId);
220  }
221
222  @Override
223  protected int getStateId(final ModifyTableState state) {
224    return state.getNumber();
225  }
226
227  @Override
228  protected ModifyTableState getInitialState() {
229    return ModifyTableState.MODIFY_TABLE_PREPARE;
230  }
231
232  @Override
233  protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException {
234    super.serializeStateData(serializer);
235
236    MasterProcedureProtos.ModifyTableStateData.Builder modifyTableMsg =
237      MasterProcedureProtos.ModifyTableStateData.newBuilder()
238        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
239        .setModifiedTableSchema(ProtobufUtil.toTableSchema(modifiedTableDescriptor))
240        .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify)
241        .setShouldCheckDescriptor(shouldCheckDescriptor);
242
243    if (unmodifiedTableDescriptor != null) {
244      modifyTableMsg
245        .setUnmodifiedTableSchema(ProtobufUtil.toTableSchema(unmodifiedTableDescriptor));
246    }
247
248    serializer.serialize(modifyTableMsg.build());
249  }
250
251  @Override
252  protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException {
253    super.deserializeStateData(serializer);
254
255    MasterProcedureProtos.ModifyTableStateData modifyTableMsg =
256      serializer.deserialize(MasterProcedureProtos.ModifyTableStateData.class);
257    setUser(MasterProcedureUtil.toUserInfo(modifyTableMsg.getUserInfo()));
258    modifiedTableDescriptor =
259      ProtobufUtil.toTableDescriptor(modifyTableMsg.getModifiedTableSchema());
260    deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify();
261    shouldCheckDescriptor =
262      modifyTableMsg.hasShouldCheckDescriptor() ? modifyTableMsg.getShouldCheckDescriptor() : false;
263
264    if (modifyTableMsg.hasUnmodifiedTableSchema()) {
265      unmodifiedTableDescriptor =
266        ProtobufUtil.toTableDescriptor(modifyTableMsg.getUnmodifiedTableSchema());
267    }
268  }
269
270  @Override
271  public TableName getTableName() {
272    return modifiedTableDescriptor.getTableName();
273  }
274
275  @Override
276  public TableOperationType getTableOperationType() {
277    return TableOperationType.EDIT;
278  }
279
280  /**
281   * Check conditions before any real action of modifying a table.
282   */
283  private void prepareModify(final MasterProcedureEnv env) throws IOException {
284    // Checks whether the table exists
285    if (!env.getMasterServices().getTableDescriptors().exists(getTableName())) {
286      throw new TableNotFoundException(getTableName());
287    }
288
289    // check that we have at least 1 CF
290    if (modifiedTableDescriptor.getColumnFamilyCount() == 0) {
291      throw new DoNotRetryIOException(
292        "Table " + getTableName().toString() + " should have at least one column family.");
293    }
294
295    // If descriptor check is enabled, check whether the table descriptor when procedure was
296    // submitted matches with the current
297    // table descriptor of the table, else retrieve the old descriptor
298    // for comparison in order to update the descriptor.
299    if (shouldCheckDescriptor) {
300      if (
301        TableDescriptor.COMPARATOR.compare(unmodifiedTableDescriptor,
302          env.getMasterServices().getTableDescriptors().get(getTableName())) != 0
303      ) {
304        LOG.error("Error while modifying table '" + getTableName().toString()
305          + "' Skipping procedure : " + this);
306        throw new ConcurrentTableModificationException(
307          "Skipping modify table operation on table '" + getTableName().toString()
308            + "' as it has already been modified by some other concurrent operation, "
309            + "Please retry.");
310      }
311    } else {
312      this.unmodifiedTableDescriptor =
313        env.getMasterServices().getTableDescriptors().get(getTableName());
314    }
315
316    this.deleteColumnFamilyInModify =
317      isDeleteColumnFamily(unmodifiedTableDescriptor, modifiedTableDescriptor);
318    if (
319      !unmodifiedTableDescriptor.getRegionServerGroup()
320        .equals(modifiedTableDescriptor.getRegionServerGroup())
321    ) {
322      Supplier<String> forWhom = () -> "table " + getTableName();
323      RSGroupInfo rsGroupInfo = MasterProcedureUtil.checkGroupExists(
324        env.getMasterServices().getRSGroupInfoManager()::getRSGroup,
325        modifiedTableDescriptor.getRegionServerGroup(), forWhom);
326      MasterProcedureUtil.checkGroupNotEmpty(rsGroupInfo, forWhom);
327    }
328
329    // check for store file tracker configurations
330    StoreFileTrackerValidationUtils.checkForModifyTable(env.getMasterConfiguration(),
331      unmodifiedTableDescriptor, modifiedTableDescriptor, !isTableEnabled(env));
332  }
333
334  /**
335   * Find out whether all column families in unmodifiedTableDescriptor also exists in the
336   * modifiedTableDescriptor.
337   * @return True if we are deleting a column family.
338   */
339  private static boolean isDeleteColumnFamily(TableDescriptor originalDescriptor,
340    TableDescriptor newDescriptor) {
341    boolean result = false;
342    final Set<byte[]> originalFamilies = originalDescriptor.getColumnFamilyNames();
343    final Set<byte[]> newFamilies = newDescriptor.getColumnFamilyNames();
344    for (byte[] familyName : originalFamilies) {
345      if (!newFamilies.contains(familyName)) {
346        result = true;
347        break;
348      }
349    }
350    return result;
351  }
352
353  /**
354   * Action before modifying table.
355   * @param env   MasterProcedureEnv
356   * @param state the procedure state
357   */
358  private void preModify(final MasterProcedureEnv env, final ModifyTableState state)
359    throws IOException, InterruptedException {
360    runCoprocessorAction(env, state);
361  }
362
363  /**
364   * Update descriptor
365   * @param env MasterProcedureEnv
366   **/
367  private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
368    env.getMasterServices().getTableDescriptors().update(modifiedTableDescriptor);
369  }
370
371  /**
372   * Removes from hdfs the families that are not longer present in the new table descriptor.
373   * @param env MasterProcedureEnv
374   */
375  private void deleteFromFs(final MasterProcedureEnv env, final TableDescriptor oldTableDescriptor,
376    final TableDescriptor newTableDescriptor) throws IOException {
377    final Set<byte[]> oldFamilies = oldTableDescriptor.getColumnFamilyNames();
378    final Set<byte[]> newFamilies = newTableDescriptor.getColumnFamilyNames();
379    for (byte[] familyName : oldFamilies) {
380      if (!newFamilies.contains(familyName)) {
381        MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(env, getTableName(),
382          getRegionInfoList(env), familyName,
383          oldTableDescriptor.getColumnFamily(familyName).isMobEnabled());
384      }
385    }
386  }
387
388  /**
389   * remove replica columns if necessary.
390   */
391  private void removeReplicaColumnsIfNeeded(MasterProcedureEnv env) throws IOException {
392    final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication();
393    final int newReplicaCount = modifiedTableDescriptor.getRegionReplication();
394    if (newReplicaCount >= oldReplicaCount) {
395      return;
396    }
397    env.getAssignmentManager().getRegionStateStore().removeRegionReplicas(getTableName(),
398      oldReplicaCount, newReplicaCount);
399    env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName()).stream()
400      .filter(r -> r.getReplicaId() >= newReplicaCount)
401      .forEach(env.getAssignmentManager().getRegionStates()::deleteRegion);
402  }
403
404  private void assignNewReplicasIfNeeded(MasterProcedureEnv env) throws IOException {
405    final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication();
406    final int newReplicaCount = modifiedTableDescriptor.getRegionReplication();
407    if (newReplicaCount <= oldReplicaCount) {
408      return;
409    }
410    if (isTableEnabled(env)) {
411      List<RegionInfo> newReplicas = env.getAssignmentManager().getRegionStates()
412        .getRegionsOfTable(getTableName()).stream().filter(RegionReplicaUtil::isDefaultReplica)
413        .flatMap(primaryRegion -> IntStream.range(oldReplicaCount, newReplicaCount).mapToObj(
414          replicaId -> RegionReplicaUtil.getRegionInfoForReplica(primaryRegion, replicaId)))
415        .collect(Collectors.toList());
416      addChildProcedure(env.getAssignmentManager().createAssignProcedures(newReplicas));
417    }
418  }
419
420  private void closeExcessReplicasIfNeeded(MasterProcedureEnv env) {
421    final int oldReplicaCount = unmodifiedTableDescriptor.getRegionReplication();
422    final int newReplicaCount = modifiedTableDescriptor.getRegionReplication();
423    if (newReplicaCount >= oldReplicaCount) {
424      return;
425    }
426    addChildProcedure(env.getAssignmentManager()
427      .createUnassignProceduresForClosingExcessRegionReplicas(getTableName(), newReplicaCount));
428  }
429
430  /**
431   * Action after modifying table.
432   * @param env   MasterProcedureEnv
433   * @param state the procedure state
434   */
435  private void postModify(final MasterProcedureEnv env, final ModifyTableState state)
436    throws IOException, InterruptedException {
437    runCoprocessorAction(env, state);
438  }
439
440  /**
441   * Coprocessor Action.
442   * @param env   MasterProcedureEnv
443   * @param state the procedure state
444   */
445  private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state)
446    throws IOException, InterruptedException {
447    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
448    if (cpHost != null) {
449      switch (state) {
450        case MODIFY_TABLE_PRE_OPERATION:
451          cpHost.preModifyTableAction(getTableName(), unmodifiedTableDescriptor,
452            modifiedTableDescriptor, getUser());
453          break;
454        case MODIFY_TABLE_POST_OPERATION:
455          cpHost.postCompletedModifyTableAction(getTableName(), unmodifiedTableDescriptor,
456            modifiedTableDescriptor, getUser());
457          break;
458        default:
459          throw new UnsupportedOperationException(this + " unhandled state=" + state);
460      }
461    }
462  }
463
464  /**
465   * Fetches all Regions for a table. Cache the result of this method if you need to use it multiple
466   * times. Be aware that it may change over in between calls to this procedure.
467   */
468  private List<RegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
469    return env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName());
470  }
471}