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.Arrays;
023import java.util.List;
024import org.apache.hadoop.hbase.HBaseIOException;
025import org.apache.hadoop.hbase.TableName;
026import org.apache.hadoop.hbase.TableNotDisabledException;
027import org.apache.hadoop.hbase.TableNotFoundException;
028import org.apache.hadoop.hbase.client.RegionInfo;
029import org.apache.hadoop.hbase.client.RegionInfoBuilder;
030import org.apache.hadoop.hbase.client.RegionReplicaUtil;
031import org.apache.hadoop.hbase.client.TableDescriptor;
032import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
033import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
034import org.apache.hadoop.hbase.util.ModifyRegionUtils;
035import org.apache.yetus.audience.InterfaceAudience;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
040import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
041import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
042import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.TruncateTableState;
043
044@InterfaceAudience.Private
045public class TruncateTableProcedure extends AbstractStateMachineTableProcedure<TruncateTableState> {
046  private static final Logger LOG = LoggerFactory.getLogger(TruncateTableProcedure.class);
047
048  private boolean preserveSplits;
049  private List<RegionInfo> regions;
050  private TableDescriptor tableDescriptor;
051  private TableName tableName;
052
053  public TruncateTableProcedure() {
054    // Required by the Procedure framework to create the procedure on replay
055    super();
056  }
057
058  public TruncateTableProcedure(final MasterProcedureEnv env, final TableName tableName,
059    boolean preserveSplits) throws HBaseIOException {
060    this(env, tableName, preserveSplits, null);
061  }
062
063  public TruncateTableProcedure(final MasterProcedureEnv env, final TableName tableName,
064    boolean preserveSplits, ProcedurePrepareLatch latch) throws HBaseIOException {
065    super(env, latch);
066    this.tableName = tableName;
067    preflightChecks(env, false);
068    this.preserveSplits = preserveSplits;
069  }
070
071  @Override
072  protected Flow executeFromState(final MasterProcedureEnv env, TruncateTableState state)
073    throws InterruptedException {
074    if (LOG.isTraceEnabled()) {
075      LOG.trace(this + " execute state=" + state);
076    }
077    try {
078      switch (state) {
079        case TRUNCATE_TABLE_PRE_OPERATION:
080          // Verify if we can truncate the table
081          if (!prepareTruncate(env)) {
082            assert isFailed() : "the truncate should have an exception here";
083            return Flow.NO_MORE_STATE;
084          }
085
086          // TODO: Move out... in the acquireLock()
087          LOG.debug("waiting for '" + getTableName() + "' regions in transition");
088          regions = env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName());
089          RegionReplicaUtil.removeNonDefaultRegions(regions);
090          assert regions != null && !regions.isEmpty() : "unexpected 0 regions";
091          ProcedureSyncWait.waitRegionInTransition(env, regions);
092
093          // Call coprocessors
094          preTruncate(env);
095
096          // We need to cache table descriptor in the initial stage, so that it's saved within
097          // the procedure stage and can get recovered if the procedure crashes between
098          // TRUNCATE_TABLE_REMOVE_FROM_META and TRUNCATE_TABLE_CREATE_FS_LAYOUT
099          tableDescriptor = env.getMasterServices().getTableDescriptors().get(tableName);
100          setNextState(TruncateTableState.TRUNCATE_TABLE_CLEAR_FS_LAYOUT);
101          break;
102        case TRUNCATE_TABLE_CLEAR_FS_LAYOUT:
103          DeleteTableProcedure.deleteFromFs(env, getTableName(), regions, true);
104          // NOTE: It's very important that we create new HRegions before next state, so that
105          // they get persisted in procedure state before we start using them for anything.
106          // Otherwise, if we create them in next step and master crashes after creating fs
107          // layout but before saving state, region re-created after recovery will have different
108          // regionId(s) and encoded names. That will lead to unwanted regions in FS layout
109          // (which were created before the crash).
110          if (!preserveSplits) {
111            // if we are not preserving splits, generate a new single region
112            regions = Arrays.asList(ModifyRegionUtils.createRegionInfos(tableDescriptor, null));
113          } else {
114            regions = recreateRegionInfo(regions);
115          }
116          setNextState(TruncateTableState.TRUNCATE_TABLE_REMOVE_FROM_META);
117          break;
118        case TRUNCATE_TABLE_REMOVE_FROM_META:
119          List<RegionInfo> originalRegions =
120            env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName());
121          DeleteTableProcedure.deleteFromMeta(env, getTableName(), originalRegions);
122          DeleteTableProcedure.deleteAssignmentState(env, getTableName());
123          setNextState(TruncateTableState.TRUNCATE_TABLE_CREATE_FS_LAYOUT);
124          break;
125        case TRUNCATE_TABLE_CREATE_FS_LAYOUT:
126          DeleteTableProcedure.deleteFromFs(env, getTableName(), regions, true);
127          regions = CreateTableProcedure.createFsLayout(env, tableDescriptor, regions);
128          env.getMasterServices().getTableDescriptors().update(tableDescriptor, true);
129          setNextState(TruncateTableState.TRUNCATE_TABLE_ADD_TO_META);
130          break;
131        case TRUNCATE_TABLE_ADD_TO_META:
132          regions = CreateTableProcedure.addTableToMeta(env, tableDescriptor, regions);
133          setNextState(TruncateTableState.TRUNCATE_TABLE_ASSIGN_REGIONS);
134          break;
135        case TRUNCATE_TABLE_ASSIGN_REGIONS:
136          CreateTableProcedure.setEnablingState(env, getTableName());
137          addChildProcedure(env.getAssignmentManager().createRoundRobinAssignProcedures(regions));
138          setNextState(TruncateTableState.TRUNCATE_TABLE_POST_OPERATION);
139          tableDescriptor = null;
140          regions = null;
141          break;
142        case TRUNCATE_TABLE_POST_OPERATION:
143          CreateTableProcedure.setEnabledState(env, getTableName());
144          postTruncate(env);
145          LOG.debug("truncate '" + getTableName() + "' completed");
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-truncate-table", e);
153      } else {
154        LOG.warn("Retriable error trying to truncate table=" + getTableName() + " state=" + state,
155          e);
156      }
157    }
158    return Flow.HAS_MORE_STATE;
159  }
160
161  @Override
162  protected void rollbackState(final MasterProcedureEnv env, final TruncateTableState state) {
163    if (state == TruncateTableState.TRUNCATE_TABLE_PRE_OPERATION) {
164      // nothing to rollback, pre-truncate is just table-state checks.
165      // We can fail if the table does not exist or is not disabled.
166      // TODO: coprocessor rollback semantic is still undefined.
167      return;
168    }
169
170    // The truncate doesn't have a rollback. The execution will succeed, at some point.
171    throw new UnsupportedOperationException("unhandled state=" + state);
172  }
173
174  @Override
175  protected void completionCleanup(final MasterProcedureEnv env) {
176    releaseSyncLatch();
177  }
178
179  @Override
180  protected boolean isRollbackSupported(final TruncateTableState state) {
181    switch (state) {
182      case TRUNCATE_TABLE_PRE_OPERATION:
183        return true;
184      default:
185        return false;
186    }
187  }
188
189  @Override
190  protected TruncateTableState getState(final int stateId) {
191    return TruncateTableState.forNumber(stateId);
192  }
193
194  @Override
195  protected int getStateId(final TruncateTableState state) {
196    return state.getNumber();
197  }
198
199  @Override
200  protected TruncateTableState getInitialState() {
201    return TruncateTableState.TRUNCATE_TABLE_PRE_OPERATION;
202  }
203
204  @Override
205  protected boolean holdLock(MasterProcedureEnv env) {
206    return true;
207  }
208
209  @Override
210  public TableName getTableName() {
211    return tableName;
212  }
213
214  @Override
215  public TableOperationType getTableOperationType() {
216    return TableOperationType.EDIT;
217  }
218
219  @Override
220  public void toStringClassDetails(StringBuilder sb) {
221    sb.append(getClass().getSimpleName());
222    sb.append(" (table=");
223    sb.append(getTableName());
224    sb.append(" preserveSplits=");
225    sb.append(preserveSplits);
226    sb.append(")");
227  }
228
229  @Override
230  protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException {
231    super.serializeStateData(serializer);
232
233    MasterProcedureProtos.TruncateTableStateData.Builder state =
234      MasterProcedureProtos.TruncateTableStateData.newBuilder()
235        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
236        .setPreserveSplits(preserveSplits);
237    if (tableDescriptor != null) {
238      state.setTableSchema(ProtobufUtil.toTableSchema(tableDescriptor));
239    } else {
240      state.setTableName(ProtobufUtil.toProtoTableName(tableName));
241    }
242    if (regions != null) {
243      for (RegionInfo hri : regions) {
244        state.addRegionInfo(ProtobufUtil.toRegionInfo(hri));
245      }
246    }
247    serializer.serialize(state.build());
248  }
249
250  @Override
251  protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException {
252    super.deserializeStateData(serializer);
253
254    MasterProcedureProtos.TruncateTableStateData state =
255      serializer.deserialize(MasterProcedureProtos.TruncateTableStateData.class);
256    setUser(MasterProcedureUtil.toUserInfo(state.getUserInfo()));
257    if (state.hasTableSchema()) {
258      tableDescriptor = ProtobufUtil.toTableDescriptor(state.getTableSchema());
259      tableName = tableDescriptor.getTableName();
260    } else {
261      tableName = ProtobufUtil.toTableName(state.getTableName());
262    }
263    preserveSplits = state.getPreserveSplits();
264    if (state.getRegionInfoCount() == 0) {
265      regions = null;
266    } else {
267      regions = new ArrayList<>(state.getRegionInfoCount());
268      for (HBaseProtos.RegionInfo hri : state.getRegionInfoList()) {
269        regions.add(ProtobufUtil.toRegionInfo(hri));
270      }
271    }
272  }
273
274  private static List<RegionInfo> recreateRegionInfo(final List<RegionInfo> regions) {
275    ArrayList<RegionInfo> newRegions = new ArrayList<>(regions.size());
276    for (RegionInfo hri : regions) {
277      newRegions.add(RegionInfoBuilder.newBuilder(hri.getTable()).setStartKey(hri.getStartKey())
278        .setEndKey(hri.getEndKey()).build());
279    }
280    return newRegions;
281  }
282
283  private boolean prepareTruncate(final MasterProcedureEnv env) throws IOException {
284    try {
285      env.getMasterServices().checkTableModifiable(getTableName());
286    } catch (TableNotFoundException | TableNotDisabledException e) {
287      setFailure("master-truncate-table", e);
288      return false;
289    }
290    return true;
291  }
292
293  private boolean preTruncate(final MasterProcedureEnv env)
294    throws IOException, InterruptedException {
295    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
296    if (cpHost != null) {
297      final TableName tableName = getTableName();
298      cpHost.preTruncateTableAction(tableName, getUser());
299    }
300    return true;
301  }
302
303  private void postTruncate(final MasterProcedureEnv env) throws IOException, InterruptedException {
304    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
305    if (cpHost != null) {
306      final TableName tableName = getTableName();
307      cpHost.postCompletedTruncateTableAction(tableName, getUser());
308    }
309  }
310
311  RegionInfo getFirstRegionInfo() {
312    if (regions == null || regions.isEmpty()) {
313      return null;
314    }
315    return regions.get(0);
316  }
317}