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