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