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