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