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