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