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