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.ArrayList;
022import java.util.List;
023import org.apache.hadoop.hbase.TableName;
024import org.apache.hadoop.hbase.TableNotDisabledException;
025import org.apache.hadoop.hbase.TableNotFoundException;
026import org.apache.hadoop.hbase.client.RegionInfo;
027import org.apache.hadoop.hbase.client.RegionReplicaUtil;
028import org.apache.hadoop.hbase.client.TableDescriptor;
029import org.apache.hadoop.hbase.client.TableState;
030import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
031import org.apache.hadoop.hbase.master.TableStateManager;
032import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
033import org.apache.yetus.audience.InterfaceAudience;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
038import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
039import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.EnableTableState;
040
041@InterfaceAudience.Private
042public class EnableTableProcedure
043    extends AbstractStateMachineTableProcedure<EnableTableState> {
044  private static final Logger LOG = LoggerFactory.getLogger(EnableTableProcedure.class);
045
046  private TableName tableName;
047
048  public EnableTableProcedure() {
049  }
050
051  /**
052   * Constructor
053   * @param env MasterProcedureEnv
054   * @param tableName the table to operate on
055   */
056  public EnableTableProcedure(MasterProcedureEnv env, TableName tableName) {
057    this(env, tableName, null);
058  }
059
060  /**
061   * Constructor
062   * @param env MasterProcedureEnv
063   * @param tableName the table to operate on
064   */
065  public EnableTableProcedure(MasterProcedureEnv env, TableName tableName,
066      ProcedurePrepareLatch syncLatch) {
067    super(env, syncLatch);
068    this.tableName = tableName;
069  }
070
071  @Override
072  protected Flow executeFromState(final MasterProcedureEnv env, final EnableTableState state)
073      throws InterruptedException {
074    LOG.trace("{} execute state={}", this, state);
075
076    try {
077      switch (state) {
078        case ENABLE_TABLE_PREPARE:
079          if (prepareEnable(env)) {
080            setNextState(EnableTableState.ENABLE_TABLE_PRE_OPERATION);
081          } else {
082            assert isFailed() : "enable should have an exception here";
083            return Flow.NO_MORE_STATE;
084          }
085          break;
086        case ENABLE_TABLE_PRE_OPERATION:
087          preEnable(env, state);
088          setNextState(EnableTableState.ENABLE_TABLE_SET_ENABLING_TABLE_STATE);
089          break;
090        case ENABLE_TABLE_SET_ENABLING_TABLE_STATE:
091          setTableStateToEnabling(env, tableName);
092          setNextState(EnableTableState.ENABLE_TABLE_MARK_REGIONS_ONLINE);
093          break;
094        case ENABLE_TABLE_MARK_REGIONS_ONLINE:
095          // Get the region replica count. If changed since disable, need to do
096          // more work assigning.
097          TableDescriptor tableDescriptor =
098              env.getMasterServices().getTableDescriptors().get(tableName);
099          int configuredReplicaCount = tableDescriptor.getRegionReplication();
100          // Get regions for the table from memory
101          List<RegionInfo> regionsOfTable =
102              env.getAssignmentManager().getRegionStates().getRegionsOfTableForEnabling(tableName);
103
104          // How many replicas do we currently have? Check regions returned from
105          // in-memory state.
106          int currentMaxReplica = getMaxReplicaId(regionsOfTable);
107          if (currentMaxReplica == configuredReplicaCount - 1) {
108            LOG.debug("No change in number of region replicas (configuredReplicaCount={});"
109              + " assigning.", configuredReplicaCount);
110          } else if (currentMaxReplica > (configuredReplicaCount - 1)) {
111            // We have additional regions as the replica count has been decreased. Delete
112            // those regions because already the table is in the unassigned state
113            LOG.warn(
114              "The number of replicas {} is more than the region replica count {}" +
115                ", usually this should not happen as we will delete them in ModifyTableProcedure",
116              currentMaxReplica + 1, configuredReplicaCount);
117            List<RegionInfo> copyOfRegions = new ArrayList<RegionInfo>(regionsOfTable);
118            for (RegionInfo regionInfo : copyOfRegions) {
119              if (regionInfo.getReplicaId() > (configuredReplicaCount - 1)) {
120                // delete the region from the regionStates
121                env.getAssignmentManager().getRegionStates().deleteRegion(regionInfo);
122                // remove it from the list of regions of the table
123                LOG.info("Removed replica={} of {}", regionInfo.getRegionId(), regionInfo);
124                regionsOfTable.remove(regionInfo);
125              }
126            }
127          } else if (currentMaxReplica < configuredReplicaCount - 1) {
128            // the replicasFound is less than the regionReplication
129            LOG.info("Number of replicas has increased for {}. Assigning new region replicas." +
130                "The previous replica count was {}. The current replica count is {}.",
131              this.tableName, currentMaxReplica + 1, configuredReplicaCount);
132            regionsOfTable = RegionReplicaUtil.addReplicas(regionsOfTable,
133              currentMaxReplica + 1, configuredReplicaCount);
134          }
135          // Assign all the table regions. (including region replicas if added).
136          // createAssignProcedure will try to retain old assignments if possible.
137          addChildProcedure(env.getAssignmentManager().createAssignProcedures(regionsOfTable));
138          setNextState(EnableTableState.ENABLE_TABLE_SET_ENABLED_TABLE_STATE);
139          break;
140        case ENABLE_TABLE_SET_ENABLED_TABLE_STATE:
141          setTableStateToEnabled(env, tableName);
142          setNextState(EnableTableState.ENABLE_TABLE_POST_OPERATION);
143          break;
144        case ENABLE_TABLE_POST_OPERATION:
145          postEnable(env, state);
146          return Flow.NO_MORE_STATE;
147        default:
148          throw new UnsupportedOperationException("unhandled state=" + state);
149      }
150    } catch (IOException e) {
151      if (isRollbackSupported(state)) {
152        setFailure("master-enable-table", e);
153      } else {
154        LOG.warn(
155          "Retriable error trying to enable table=" + tableName + " (in state=" + state + ")", e);
156      }
157    }
158    return Flow.HAS_MORE_STATE;
159  }
160
161  @Override
162  protected void rollbackState(final MasterProcedureEnv env, final EnableTableState state)
163      throws IOException {
164    // nothing to rollback, prepare-disable is just table-state checks.
165    // We can fail if the table does not exist or is not disabled.
166    switch (state) {
167      case ENABLE_TABLE_PRE_OPERATION:
168        return;
169      case ENABLE_TABLE_PREPARE:
170        releaseSyncLatch();
171        return;
172      default:
173        break;
174    }
175
176    // The delete doesn't have a rollback. The execution will succeed, at some point.
177    throw new UnsupportedOperationException("unhandled state=" + state);
178  }
179
180  @Override
181  protected boolean isRollbackSupported(final EnableTableState state) {
182    switch (state) {
183      case ENABLE_TABLE_PREPARE:
184      case ENABLE_TABLE_PRE_OPERATION:
185        return true;
186      default:
187        return false;
188    }
189  }
190
191  @Override
192  protected EnableTableState getState(final int stateId) {
193    return EnableTableState.forNumber(stateId);
194  }
195
196  @Override
197  protected int getStateId(final EnableTableState state) {
198    return state.getNumber();
199  }
200
201  @Override
202  protected EnableTableState getInitialState() {
203    return EnableTableState.ENABLE_TABLE_PREPARE;
204  }
205
206  @Override
207  protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException {
208    super.serializeStateData(serializer);
209
210    // the skipTableStateCheck is false so we still need to set it...
211    @SuppressWarnings("deprecation")
212    MasterProcedureProtos.EnableTableStateData.Builder enableTableMsg =
213      MasterProcedureProtos.EnableTableStateData.newBuilder()
214        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
215        .setTableName(ProtobufUtil.toProtoTableName(tableName)).setSkipTableStateCheck(false);
216
217    serializer.serialize(enableTableMsg.build());
218  }
219
220  @Override
221  protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException {
222    super.deserializeStateData(serializer);
223
224    MasterProcedureProtos.EnableTableStateData enableTableMsg =
225      serializer.deserialize(MasterProcedureProtos.EnableTableStateData.class);
226    setUser(MasterProcedureUtil.toUserInfo(enableTableMsg.getUserInfo()));
227    tableName = ProtobufUtil.toTableName(enableTableMsg.getTableName());
228  }
229
230  @Override
231  public TableName getTableName() {
232    return tableName;
233  }
234
235  @Override
236  public TableOperationType getTableOperationType() {
237    return TableOperationType.ENABLE;
238  }
239
240
241  /**
242   * Action before any real action of enabling table. Set the exception in the procedure instead
243   * of throwing it.  This approach is to deal with backward compatible with 1.0.
244   * @param env MasterProcedureEnv
245   * @return whether the table passes the necessary checks
246   * @throws IOException
247   */
248  private boolean prepareEnable(final MasterProcedureEnv env) throws IOException {
249    boolean canTableBeEnabled = true;
250    // Check whether table exists
251    if (!env.getMasterServices().getTableDescriptors().exists(tableName)) {
252      setFailure("master-enable-table", new TableNotFoundException(tableName));
253      canTableBeEnabled = false;
254    } else {
255      // There could be multiple client requests trying to disable or enable
256      // the table at the same time. Ensure only the first request is honored
257      // After that, no other requests can be accepted until the table reaches
258      // DISABLED or ENABLED.
259      //
260      // Note: in 1.0 release, we called TableStateManager.setTableStateIfInStates() to set
261      // the state to ENABLING from DISABLED. The implementation was done before table lock
262      // was implemented. With table lock, there is no need to set the state here (it will
263      // set the state later on). A quick state check should be enough for us to move forward.
264      TableStateManager tsm = env.getMasterServices().getTableStateManager();
265      TableState ts = tsm.getTableState(tableName);
266      if(!ts.isDisabled()){
267        LOG.info("Not DISABLED tableState={}; skipping enable; {}", ts.getState(), this);
268        setFailure("master-enable-table", new TableNotDisabledException(ts.toString()));
269        canTableBeEnabled = false;
270      }
271    }
272
273    // We are done the check. Future actions in this procedure could be done asynchronously.
274    releaseSyncLatch();
275
276    return canTableBeEnabled;
277  }
278
279  /**
280   * Action before enabling table.
281   * @param env MasterProcedureEnv
282   * @param state the procedure state
283   * @throws IOException
284   * @throws InterruptedException
285   */
286  private void preEnable(final MasterProcedureEnv env, final EnableTableState state)
287      throws IOException, InterruptedException {
288    runCoprocessorAction(env, state);
289  }
290
291  /**
292   * Mark table state to Enabling
293   * @param env MasterProcedureEnv
294   * @param tableName the target table
295   * @throws IOException
296   */
297  protected static void setTableStateToEnabling(
298      final MasterProcedureEnv env,
299      final TableName tableName) throws IOException {
300    // Set table disabling flag up in zk.
301    LOG.info("Attempting to enable the table " + tableName);
302    env.getMasterServices().getTableStateManager().setTableState(
303      tableName,
304      TableState.State.ENABLING);
305  }
306
307  /**
308   * Mark table state to Enabled
309   * @param env MasterProcedureEnv
310   * @throws IOException
311   */
312  protected static void setTableStateToEnabled(
313      final MasterProcedureEnv env,
314      final TableName tableName) throws IOException {
315    // Flip the table to Enabled
316    env.getMasterServices().getTableStateManager().setTableState(
317      tableName,
318      TableState.State.ENABLED);
319    LOG.info("Table '" + tableName + "' was successfully enabled.");
320  }
321
322  /**
323   * Action after enabling table.
324   * @param env MasterProcedureEnv
325   * @param state the procedure state
326   * @throws IOException
327   * @throws InterruptedException
328   */
329  private void postEnable(final MasterProcedureEnv env, final EnableTableState state)
330      throws IOException, InterruptedException {
331    runCoprocessorAction(env, state);
332  }
333
334  /**
335   * Coprocessor Action.
336   * @param env MasterProcedureEnv
337   * @param state the procedure state
338   * @throws IOException
339   * @throws InterruptedException
340   */
341  private void runCoprocessorAction(final MasterProcedureEnv env, final EnableTableState state)
342      throws IOException, InterruptedException {
343    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
344    if (cpHost != null) {
345      switch (state) {
346        case ENABLE_TABLE_PRE_OPERATION:
347          cpHost.preEnableTableAction(getTableName(), getUser());
348          break;
349        case ENABLE_TABLE_POST_OPERATION:
350          cpHost.postCompletedEnableTableAction(getTableName(), getUser());
351          break;
352        default:
353          throw new UnsupportedOperationException(this + " unhandled state=" + state);
354      }
355    }
356  }
357
358  /**
359   * @return Maximum region replica id found in passed list of regions.
360   */
361  private static int getMaxReplicaId(List<RegionInfo> regions) {
362    int max = 0;
363    for (RegionInfo regionInfo: regions) {
364      if (regionInfo.getReplicaId() > max) {
365        // Iterating through all the list to identify the highest replicaID region.
366        // We can stop after checking with the first set of regions??
367        max = regionInfo.getReplicaId();
368      }
369    }
370    return max;
371
372  }
373}