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