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