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