|
18 | 18 | package org.apache.hadoop.hbase.master.cleaner; |
19 | 19 |
|
20 | 20 | import java.io.IOException; |
21 | | -import java.util.Collections; |
| 21 | +import java.util.ArrayList; |
| 22 | +import java.util.Arrays; |
22 | 23 | import java.util.Comparator; |
23 | 24 | import java.util.HashMap; |
24 | 25 | import java.util.LinkedList; |
25 | 26 | import java.util.List; |
26 | 27 | import java.util.Map; |
27 | | -import java.util.Optional; |
28 | | -import java.util.concurrent.ExecutionException; |
29 | | -import java.util.concurrent.RecursiveTask; |
| 28 | +import java.util.concurrent.CompletableFuture; |
30 | 29 | import java.util.concurrent.atomic.AtomicBoolean; |
| 30 | +import java.util.stream.Collectors; |
| 31 | + |
31 | 32 | import org.apache.hadoop.conf.Configuration; |
32 | 33 | import org.apache.hadoop.fs.FileStatus; |
33 | 34 | import org.apache.hadoop.fs.FileSystem; |
34 | 35 | import org.apache.hadoop.fs.Path; |
35 | 36 | import org.apache.hadoop.fs.PathIsNotEmptyDirectoryException; |
36 | 37 | import org.apache.hadoop.hbase.ScheduledChore; |
37 | 38 | import org.apache.hadoop.hbase.Stoppable; |
38 | | -import org.apache.hadoop.hbase.util.FSUtils; |
| 39 | +import org.apache.hadoop.hbase.util.FutureUtils; |
39 | 40 | import org.apache.hadoop.ipc.RemoteException; |
40 | 41 | import org.apache.yetus.audience.InterfaceAudience; |
41 | 42 | import org.slf4j.Logger; |
42 | 43 | import org.slf4j.LoggerFactory; |
43 | 44 |
|
44 | 45 | import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; |
45 | 46 | import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; |
46 | | -import org.apache.hbase.thirdparty.com.google.common.base.Predicate; |
47 | 47 | import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableSet; |
48 | 48 | import org.apache.hbase.thirdparty.com.google.common.collect.Iterables; |
49 | 49 | import org.apache.hbase.thirdparty.com.google.common.collect.Lists; |
@@ -211,11 +211,16 @@ private void preRunCleaner() { |
211 | 211 | cleanersChain.forEach(FileCleanerDelegate::preClean); |
212 | 212 | } |
213 | 213 |
|
214 | | - public Boolean runCleaner() { |
| 214 | + public boolean runCleaner() { |
215 | 215 | preRunCleaner(); |
216 | | - CleanerTask task = new CleanerTask(this.oldFileDir, true); |
217 | | - pool.execute(task); |
218 | | - return task.join(); |
| 216 | + try { |
| 217 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); |
| 218 | + pool.execute(() -> traverseAndDelete(oldFileDir, true, future)); |
| 219 | + return future.get(); |
| 220 | + } catch (Exception e) { |
| 221 | + LOG.info("Failed to traverse and delete the dir: {}", oldFileDir, e); |
| 222 | + return false; |
| 223 | + } |
219 | 224 | } |
220 | 225 |
|
221 | 226 | /** |
@@ -360,126 +365,97 @@ public boolean setEnabled(final boolean enabled) { |
360 | 365 | } |
361 | 366 |
|
362 | 367 | private interface Action<T> { |
363 | | - T act() throws IOException; |
| 368 | + T act() throws Exception; |
364 | 369 | } |
365 | 370 |
|
366 | 371 | /** |
367 | | - * Attemps to clean up a directory, its subdirectories, and files. Return value is true if |
368 | | - * everything was deleted. false on partial / total failures. |
| 372 | + * Attempts to clean up a directory(its subdirectories, and files) in a |
| 373 | + * {@link java.util.concurrent.ThreadPoolExecutor} concurrently. We can get the final result by |
| 374 | + * calling result.get(). |
369 | 375 | */ |
370 | | - private final class CleanerTask extends RecursiveTask<Boolean> { |
371 | | - |
372 | | - private static final long serialVersionUID = -5444212174088754172L; |
373 | | - |
374 | | - private final Path dir; |
375 | | - private final boolean root; |
376 | | - |
377 | | - CleanerTask(final FileStatus dir, final boolean root) { |
378 | | - this(dir.getPath(), root); |
379 | | - } |
380 | | - |
381 | | - CleanerTask(final Path dir, final boolean root) { |
382 | | - this.dir = dir; |
383 | | - this.root = root; |
384 | | - } |
385 | | - |
386 | | - @Override |
387 | | - protected Boolean compute() { |
388 | | - LOG.trace("Cleaning under {}", dir); |
389 | | - List<FileStatus> subDirs; |
390 | | - List<FileStatus> files; |
391 | | - try { |
392 | | - // if dir doesn't exist, we'll get null back for both of these |
393 | | - // which will fall through to succeeding. |
394 | | - subDirs = getFilteredStatus(FileStatus::isDirectory); |
395 | | - files = getFilteredStatus(FileStatus::isFile); |
396 | | - } catch (IOException ioe) { |
397 | | - LOG.warn("failed to get FileStatus for contents of '{}'", dir, ioe); |
398 | | - return false; |
399 | | - } |
400 | | - |
401 | | - boolean allFilesDeleted = true; |
402 | | - if (!files.isEmpty()) { |
403 | | - allFilesDeleted = deleteAction(() -> checkAndDeleteFiles(files), "files"); |
404 | | - } |
405 | | - |
406 | | - boolean allSubdirsDeleted = true; |
| 376 | + private void traverseAndDelete(Path dir, boolean root, CompletableFuture<Boolean> result) { |
| 377 | + try { |
| 378 | + // Step.1: List all files under the given directory. |
| 379 | + List<FileStatus> allPaths = Arrays.asList(fs.listStatus(dir)); |
| 380 | + List<FileStatus> subDirs = |
| 381 | + allPaths.stream().filter(FileStatus::isDirectory).collect(Collectors.toList()); |
| 382 | + List<FileStatus> files = |
| 383 | + allPaths.stream().filter(FileStatus::isFile).collect(Collectors.toList()); |
| 384 | + |
| 385 | + // Step.2: Try to delete all the deletable files. |
| 386 | + boolean allFilesDeleted = |
| 387 | + files.isEmpty() || deleteAction(() -> checkAndDeleteFiles(files), "files", dir); |
| 388 | + |
| 389 | + // Step.3: Start to traverse and delete the sub-directories. |
| 390 | + List<CompletableFuture<Boolean>> futures = new ArrayList<>(); |
407 | 391 | if (!subDirs.isEmpty()) { |
408 | | - List<CleanerTask> tasks = Lists.newArrayListWithCapacity(subDirs.size()); |
409 | 392 | sortByConsumedSpace(subDirs); |
410 | | - for (FileStatus subdir : subDirs) { |
411 | | - CleanerTask task = new CleanerTask(subdir, false); |
412 | | - tasks.add(task); |
413 | | - task.fork(); |
414 | | - } |
415 | | - allSubdirsDeleted = deleteAction(() -> getCleanResult(tasks), "subdirs"); |
| 393 | + // Submit the request of sub-directory deletion. |
| 394 | + subDirs.forEach(subDir -> { |
| 395 | + CompletableFuture<Boolean> subFuture = new CompletableFuture<>(); |
| 396 | + pool.execute(() -> traverseAndDelete(subDir.getPath(), false, subFuture)); |
| 397 | + futures.add(subFuture); |
| 398 | + }); |
416 | 399 | } |
417 | 400 |
|
418 | | - boolean result = allFilesDeleted && allSubdirsDeleted; |
419 | | - // if and only if files and subdirs under current dir are deleted successfully, and |
420 | | - // it is not the root dir, then task will try to delete it. |
421 | | - if (result && !root) { |
422 | | - result &= deleteAction(() -> fs.delete(dir, false), "dir"); |
423 | | - } |
424 | | - return result; |
425 | | - } |
426 | | - |
427 | | - /** |
428 | | - * Get FileStatus with filter. |
429 | | - * @param function a filter function |
430 | | - * @return filtered FileStatus or empty list if dir doesn't exist |
431 | | - * @throws IOException if there's an error other than dir not existing |
432 | | - */ |
433 | | - private List<FileStatus> getFilteredStatus(Predicate<FileStatus> function) throws IOException { |
434 | | - return Optional.ofNullable(FSUtils.listStatusWithStatusFilter(fs, dir, |
435 | | - status -> function.test(status))).orElseGet(Collections::emptyList); |
436 | | - } |
437 | | - |
438 | | - /** |
439 | | - * Perform a delete on a specified type. |
440 | | - * @param deletion a delete |
441 | | - * @param type possible values are 'files', 'subdirs', 'dirs' |
442 | | - * @return true if it deleted successfully, false otherwise |
443 | | - */ |
444 | | - private boolean deleteAction(Action<Boolean> deletion, String type) { |
445 | | - boolean deleted; |
446 | | - try { |
447 | | - LOG.trace("Start deleting {} under {}", type, dir); |
448 | | - deleted = deletion.act(); |
449 | | - } catch (PathIsNotEmptyDirectoryException exception) { |
450 | | - // N.B. HDFS throws this exception when we try to delete a non-empty directory, but |
451 | | - // LocalFileSystem throws a bare IOException. So some test code will get the verbose |
452 | | - // message below. |
453 | | - LOG.debug("Couldn't delete '{}' yet because it isn't empty. Probably transient. " + |
454 | | - "exception details at TRACE.", dir); |
455 | | - LOG.trace("Couldn't delete '{}' yet because it isn't empty w/exception.", dir, exception); |
456 | | - deleted = false; |
457 | | - } catch (IOException ioe) { |
458 | | - LOG.info("Could not delete {} under {}. might be transient; we'll retry. if it keeps " + |
459 | | - "happening, use following exception when asking on mailing list.", |
460 | | - type, dir, ioe); |
461 | | - deleted = false; |
462 | | - } |
463 | | - LOG.trace("Finish deleting {} under {}, deleted=", type, dir, deleted); |
464 | | - return deleted; |
| 401 | + // Step.4: Once all sub-files & sub-directories are deleted, then can try to delete the |
| 402 | + // current directory asynchronously. |
| 403 | + FutureUtils.addListener( |
| 404 | + CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])), |
| 405 | + (voidObj, e) -> { |
| 406 | + if (e != null) { |
| 407 | + result.completeExceptionally(e); |
| 408 | + return; |
| 409 | + } |
| 410 | + try { |
| 411 | + boolean allSubDirsDeleted = futures.stream().allMatch(CompletableFuture::join); |
| 412 | + boolean deleted = allFilesDeleted && allSubDirsDeleted; |
| 413 | + if (deleted && !root) { |
| 414 | + // If and only if files and sub-dirs under current dir are deleted successfully, and |
| 415 | + // the empty directory can be deleted, and it is not the root dir then task will |
| 416 | + // try to delete it. |
| 417 | + deleted = deleteAction(() -> fs.delete(dir, false), "dir", dir); |
| 418 | + } |
| 419 | + result.complete(deleted); |
| 420 | + } catch (Exception ie) { |
| 421 | + // Must handle the inner exception here, otherwise the result may get stuck if one |
| 422 | + // sub-directory get some failure. |
| 423 | + result.completeExceptionally(ie); |
| 424 | + } |
| 425 | + }); |
| 426 | + } catch (Exception e) { |
| 427 | + LOG.debug("Failed to traverse and delete the path: {}", dir, e); |
| 428 | + result.completeExceptionally(e); |
465 | 429 | } |
| 430 | + } |
466 | 431 |
|
467 | | - /** |
468 | | - * Get cleaner results of subdirs. |
469 | | - * @param tasks subdirs cleaner tasks |
470 | | - * @return true if all subdirs deleted successfully, false for patial/all failures |
471 | | - * @throws IOException something happen during computation |
472 | | - */ |
473 | | - private boolean getCleanResult(List<CleanerTask> tasks) throws IOException { |
474 | | - boolean cleaned = true; |
475 | | - try { |
476 | | - for (CleanerTask task : tasks) { |
477 | | - cleaned &= task.get(); |
478 | | - } |
479 | | - } catch (InterruptedException | ExecutionException e) { |
480 | | - throw new IOException(e); |
481 | | - } |
482 | | - return cleaned; |
| 432 | + /** |
| 433 | + * Perform a delete on a specified type. |
| 434 | + * @param deletion a delete |
| 435 | + * @param type possible values are 'files', 'subdirs', 'dirs' |
| 436 | + * @return true if it deleted successfully, false otherwise |
| 437 | + */ |
| 438 | + private boolean deleteAction(Action<Boolean> deletion, String type, Path dir) { |
| 439 | + boolean deleted; |
| 440 | + try { |
| 441 | + LOG.trace("Start deleting {} under {}", type, dir); |
| 442 | + deleted = deletion.act(); |
| 443 | + } catch (PathIsNotEmptyDirectoryException exception) { |
| 444 | + // N.B. HDFS throws this exception when we try to delete a non-empty directory, but |
| 445 | + // LocalFileSystem throws a bare IOException. So some test code will get the verbose |
| 446 | + // message below. |
| 447 | + LOG.debug("Couldn't delete '{}' yet because it isn't empty w/exception.", dir, exception); |
| 448 | + deleted = false; |
| 449 | + } catch (IOException ioe) { |
| 450 | + LOG.info("Could not delete {} under {}. might be transient; we'll retry. if it keeps " |
| 451 | + + "happening, use following exception when asking on mailing list.", |
| 452 | + type, dir, ioe); |
| 453 | + deleted = false; |
| 454 | + } catch (Exception e) { |
| 455 | + LOG.info("unexpected exception: ", e); |
| 456 | + deleted = false; |
483 | 457 | } |
| 458 | + LOG.trace("Finish deleting {} under {}, deleted=", type, dir, deleted); |
| 459 | + return deleted; |
484 | 460 | } |
485 | 461 | } |
0 commit comments