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;
024import org.apache.hadoop.fs.FileStatus;
025import org.apache.hadoop.fs.FileSystem;
026import org.apache.hadoop.fs.Path;
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.hadoop.hbase.backup.HFileArchiver;
032import org.apache.hadoop.hbase.client.Connection;
033import org.apache.hadoop.hbase.client.Delete;
034import org.apache.hadoop.hbase.client.RegionInfo;
035import org.apache.hadoop.hbase.client.Result;
036import org.apache.hadoop.hbase.client.ResultScanner;
037import org.apache.hadoop.hbase.client.Scan;
038import org.apache.hadoop.hbase.client.Table;
039import org.apache.hadoop.hbase.favored.FavoredNodesManager;
040import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
041import org.apache.hadoop.hbase.master.MasterFileSystem;
042import org.apache.hadoop.hbase.mob.MobConstants;
043import org.apache.hadoop.hbase.mob.MobUtils;
044import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
045import org.apache.hadoop.hbase.util.FSUtils;
046import org.apache.yetus.audience.InterfaceAudience;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
051import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
052import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
053import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.DeleteTableState;
054
055@InterfaceAudience.Private
056public class DeleteTableProcedure
057    extends AbstractStateMachineTableProcedure<DeleteTableState> {
058  private static final Logger LOG = LoggerFactory.getLogger(DeleteTableProcedure.class);
059
060  private List<RegionInfo> regions;
061  private TableName tableName;
062
063  public DeleteTableProcedure() {
064    // Required by the Procedure framework to create the procedure on replay
065    super();
066  }
067
068  public DeleteTableProcedure(final MasterProcedureEnv env, final TableName tableName) {
069    this(env, tableName, null);
070  }
071
072  public DeleteTableProcedure(final MasterProcedureEnv env, final TableName tableName,
073      final ProcedurePrepareLatch syncLatch) {
074    super(env, syncLatch);
075    this.tableName = tableName;
076  }
077
078  @Override
079  protected Flow executeFromState(final MasterProcedureEnv env, DeleteTableState state)
080      throws InterruptedException {
081    if (LOG.isTraceEnabled()) {
082      LOG.trace(this + " execute state=" + state);
083    }
084    try {
085      switch (state) {
086        case DELETE_TABLE_PRE_OPERATION:
087          // Verify if we can delete the table
088          boolean deletable = prepareDelete(env);
089          releaseSyncLatch();
090          if (!deletable) {
091            assert isFailed() : "the delete should have an exception here";
092            return Flow.NO_MORE_STATE;
093          }
094
095          // TODO: Move out... in the acquireLock()
096          LOG.debug("Waiting for RIT for {}", this);
097          regions = env.getAssignmentManager().getRegionStates().getRegionsOfTable(getTableName());
098          assert regions != null && !regions.isEmpty() : "unexpected 0 regions";
099          ProcedureSyncWait.waitRegionInTransition(env, regions);
100
101          // Call coprocessors
102          preDelete(env);
103
104          setNextState(DeleteTableState.DELETE_TABLE_REMOVE_FROM_META);
105          break;
106        case DELETE_TABLE_REMOVE_FROM_META:
107          LOG.debug("Deleting regions from META for {}", this);
108          DeleteTableProcedure.deleteFromMeta(env, getTableName(), regions);
109          setNextState(DeleteTableState.DELETE_TABLE_CLEAR_FS_LAYOUT);
110          break;
111        case DELETE_TABLE_CLEAR_FS_LAYOUT:
112          LOG.debug("Deleting regions from filesystem for {}", this);
113          DeleteTableProcedure.deleteFromFs(env, getTableName(), regions, true);
114          setNextState(DeleteTableState.DELETE_TABLE_UPDATE_DESC_CACHE);
115          regions = null;
116          break;
117        case DELETE_TABLE_UPDATE_DESC_CACHE:
118          LOG.debug("Deleting descriptor for {}", this);
119          DeleteTableProcedure.deleteTableDescriptorCache(env, getTableName());
120          setNextState(DeleteTableState.DELETE_TABLE_UNASSIGN_REGIONS);
121          break;
122        case DELETE_TABLE_UNASSIGN_REGIONS:
123          LOG.debug("Deleting assignment state for {}", this);
124          DeleteTableProcedure.deleteAssignmentState(env, getTableName());
125          setNextState(DeleteTableState.DELETE_TABLE_POST_OPERATION);
126          break;
127        case DELETE_TABLE_POST_OPERATION:
128          postDelete(env);
129          LOG.debug("Finished {}", this);
130          return Flow.NO_MORE_STATE;
131        default:
132          throw new UnsupportedOperationException("unhandled state=" + state);
133      }
134    } catch (IOException e) {
135      if (isRollbackSupported(state)) {
136        setFailure("master-delete-table", e);
137      } else {
138        LOG.warn("Retriable error trying to delete table=" + getTableName() + " state=" + state, e);
139      }
140    }
141    return Flow.HAS_MORE_STATE;
142  }
143
144  @Override
145  protected boolean abort(MasterProcedureEnv env) {
146    // TODO: Current behavior is: with no rollback and no abort support, procedure may get stuck
147    // looping in retrying failing a step forever. Default behavior of abort is changed to support
148    // aborting all procedures. Override the default wisely. Following code retains the current
149    // behavior. Revisit it later.
150    return isRollbackSupported(getCurrentState()) ? super.abort(env) : false;
151  }
152
153  @Override
154  protected void rollbackState(final MasterProcedureEnv env, final DeleteTableState state) {
155    if (state == DeleteTableState.DELETE_TABLE_PRE_OPERATION) {
156      // nothing to rollback, pre-delete is just table-state checks.
157      // We can fail if the table does not exist or is not disabled.
158      // TODO: coprocessor rollback semantic is still undefined.
159      releaseSyncLatch();
160      return;
161    }
162
163    // The delete doesn't have a rollback. The execution will succeed, at some point.
164    throw new UnsupportedOperationException("unhandled state=" + state);
165  }
166
167  @Override
168  protected boolean isRollbackSupported(final DeleteTableState state) {
169    switch (state) {
170      case DELETE_TABLE_PRE_OPERATION:
171        return true;
172      default:
173        return false;
174    }
175  }
176
177  @Override
178  protected DeleteTableState getState(final int stateId) {
179    return DeleteTableState.valueOf(stateId);
180  }
181
182  @Override
183  protected int getStateId(final DeleteTableState state) {
184    return state.getNumber();
185  }
186
187  @Override
188  protected DeleteTableState getInitialState() {
189    return DeleteTableState.DELETE_TABLE_PRE_OPERATION;
190  }
191
192  @Override
193  public TableName getTableName() {
194    return tableName;
195  }
196
197  @Override
198  public TableOperationType getTableOperationType() {
199    return TableOperationType.DELETE;
200  }
201
202  @Override
203  protected void serializeStateData(ProcedureStateSerializer serializer)
204      throws IOException {
205    super.serializeStateData(serializer);
206
207    MasterProcedureProtos.DeleteTableStateData.Builder state =
208      MasterProcedureProtos.DeleteTableStateData.newBuilder()
209        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
210        .setTableName(ProtobufUtil.toProtoTableName(tableName));
211    if (regions != null) {
212      for (RegionInfo hri: regions) {
213        state.addRegionInfo(ProtobufUtil.toRegionInfo(hri));
214      }
215    }
216    serializer.serialize(state.build());
217  }
218
219  @Override
220  protected void deserializeStateData(ProcedureStateSerializer serializer)
221      throws IOException {
222    super.deserializeStateData(serializer);
223
224    MasterProcedureProtos.DeleteTableStateData state =
225        serializer.deserialize(MasterProcedureProtos.DeleteTableStateData.class);
226    setUser(MasterProcedureUtil.toUserInfo(state.getUserInfo()));
227    tableName = ProtobufUtil.toTableName(state.getTableName());
228    if (state.getRegionInfoCount() == 0) {
229      regions = null;
230    } else {
231      regions = new ArrayList<>(state.getRegionInfoCount());
232      for (HBaseProtos.RegionInfo hri: state.getRegionInfoList()) {
233        regions.add(ProtobufUtil.toRegionInfo(hri));
234      }
235    }
236  }
237
238  private boolean prepareDelete(final MasterProcedureEnv env) throws IOException {
239    try {
240      env.getMasterServices().checkTableModifiable(tableName);
241    } catch (TableNotFoundException|TableNotDisabledException e) {
242      setFailure("master-delete-table", e);
243      return false;
244    }
245    return true;
246  }
247
248  private boolean preDelete(final MasterProcedureEnv env)
249      throws IOException, InterruptedException {
250    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
251    if (cpHost != null) {
252      final TableName tableName = this.tableName;
253      cpHost.preDeleteTableAction(tableName, getUser());
254    }
255    return true;
256  }
257
258  private void postDelete(final MasterProcedureEnv env)
259      throws IOException, InterruptedException {
260    deleteTableStates(env, tableName);
261
262    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
263    if (cpHost != null) {
264      final TableName tableName = this.tableName;
265      cpHost.postCompletedDeleteTableAction(tableName, getUser());
266    }
267  }
268
269  protected static void deleteFromFs(final MasterProcedureEnv env,
270      final TableName tableName, final List<RegionInfo> regions,
271      final boolean archive) throws IOException {
272    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
273    final FileSystem fs = mfs.getFileSystem();
274    final Path tempdir = mfs.getTempDir();
275
276    final Path tableDir = FSUtils.getTableDir(mfs.getRootDir(), tableName);
277    final Path tempTableDir = FSUtils.getTableDir(tempdir, tableName);
278
279    if (fs.exists(tableDir)) {
280      // Ensure temp exists
281      if (!fs.exists(tempdir) && !fs.mkdirs(tempdir)) {
282        throw new IOException("HBase temp directory '" + tempdir + "' creation failure.");
283      }
284
285      // Ensure parent exists
286      if (!fs.exists(tempTableDir.getParent()) && !fs.mkdirs(tempTableDir.getParent())) {
287        throw new IOException("HBase temp directory '" + tempdir + "' creation failure.");
288      }
289
290      // Move the table in /hbase/.tmp
291      if (!fs.rename(tableDir, tempTableDir)) {
292        if (fs.exists(tempTableDir)) {
293          // TODO
294          // what's in this dir? something old? probably something manual from the user...
295          // let's get rid of this stuff...
296          FileStatus[] files = fs.listStatus(tempdir);
297          if (files != null && files.length > 0) {
298            for (int i = 0; i < files.length; ++i) {
299              if (!files[i].isDir()) continue;
300              HFileArchiver.archiveRegion(fs, mfs.getRootDir(), tempTableDir, files[i].getPath());
301            }
302          }
303          fs.delete(tempdir, true);
304        }
305        throw new IOException("Unable to move '" + tableDir + "' to temp '" + tempTableDir + "'");
306      }
307    }
308
309    // Archive regions from FS (temp directory)
310    if (archive) {
311      for (RegionInfo hri : regions) {
312        LOG.debug("Archiving region " + hri.getRegionNameAsString() + " from FS");
313        HFileArchiver.archiveRegion(fs, mfs.getRootDir(), tempTableDir,
314          FSUtils.getRegionDirFromTableDir(tempTableDir, hri));
315      }
316      LOG.debug("Table '" + tableName + "' archived!");
317    }
318
319    // Archive mob data
320    Path mobTableDir = FSUtils.getTableDir(new Path(mfs.getRootDir(), MobConstants.MOB_DIR_NAME),
321            tableName);
322    Path regionDir =
323            new Path(mobTableDir, MobUtils.getMobRegionInfo(tableName).getEncodedName());
324    if (fs.exists(regionDir)) {
325      HFileArchiver.archiveRegion(fs, mfs.getRootDir(), mobTableDir, regionDir);
326    }
327
328    // Delete table directory from FS (temp directory)
329    if (!fs.delete(tempTableDir, true) && fs.exists(tempTableDir)) {
330      throw new IOException("Couldn't delete " + tempTableDir);
331    }
332
333    // Delete the table directory where the mob files are saved
334    if (mobTableDir != null && fs.exists(mobTableDir)) {
335      if (!fs.delete(mobTableDir, true)) {
336        throw new IOException("Couldn't delete mob dir " + mobTableDir);
337      }
338    }
339
340    // Delete the directory on wal filesystem
341    FileSystem walFs = mfs.getWALFileSystem();
342    Path tableWALDir = FSUtils.getWALTableDir(env.getMasterConfiguration(), tableName);
343    if (walFs.exists(tableWALDir) && !walFs.delete(tableWALDir, true)) {
344      throw new IOException("Couldn't delete table dir on wal filesystem" + tableWALDir);
345    }
346  }
347
348  /**
349   * There may be items for this table still up in hbase:meta in the case where the
350   * info:regioninfo column was empty because of some write error. Remove ALL rows from hbase:meta
351   * that have to do with this table. See HBASE-12980.
352   * @throws IOException
353   */
354  private static void cleanRegionsInMeta(final MasterProcedureEnv env, final TableName tableName)
355      throws IOException {
356    Connection connection = env.getMasterServices().getConnection();
357    Scan tableScan = MetaTableAccessor.getScanForTableName(connection, tableName);
358    try (Table metaTable = connection.getTable(TableName.META_TABLE_NAME)) {
359      List<Delete> deletes = new ArrayList<>();
360      try (ResultScanner resScanner = metaTable.getScanner(tableScan)) {
361        for (Result result : resScanner) {
362          deletes.add(new Delete(result.getRow()));
363        }
364      }
365      if (!deletes.isEmpty()) {
366        LOG.warn("Deleting some vestigial " + deletes.size() + " rows of " + tableName + " from "
367            + TableName.META_TABLE_NAME);
368        metaTable.delete(deletes);
369      }
370    }
371  }
372
373  protected static void deleteFromMeta(final MasterProcedureEnv env, final TableName tableName,
374      List<RegionInfo> regions) throws IOException {
375    // Clean any remaining rows for this table.
376    cleanRegionsInMeta(env, tableName);
377
378    // clean region references from the server manager
379    env.getMasterServices().getServerManager().removeRegions(regions);
380
381    // Clear Favored Nodes for this table
382    FavoredNodesManager fnm = env.getMasterServices().getFavoredNodesManager();
383    if (fnm != null) {
384      fnm.deleteFavoredNodesForRegions(regions);
385    }
386  }
387
388  protected static void deleteAssignmentState(final MasterProcedureEnv env,
389      final TableName tableName) throws IOException {
390    // Clean up regions of the table in RegionStates.
391    LOG.debug("Removing '" + tableName + "' from region states.");
392    env.getMasterServices().getAssignmentManager().deleteTable(tableName);
393
394    // If entry for this table states, remove it.
395    LOG.debug("Marking '" + tableName + "' as deleted.");
396    env.getMasterServices().getTableStateManager().setDeletedTable(tableName);
397  }
398
399  protected static void deleteTableDescriptorCache(final MasterProcedureEnv env,
400      final TableName tableName) throws IOException {
401    LOG.debug("Removing '" + tableName + "' descriptor.");
402    env.getMasterServices().getTableDescriptors().remove(tableName);
403  }
404
405  protected static void deleteTableStates(final MasterProcedureEnv env, final TableName tableName)
406      throws IOException {
407    if (!tableName.isSystemTable()) {
408      ProcedureSyncWait.getMasterQuotaManager(env).removeTableFromNamespaceQuota(tableName);
409    }
410  }
411}