slawekjaranowski commented on code in PR #347:
URL:
https://github.com/apache/maven-clean-plugin/pull/347#discussion_r4111526899
##########
src/main/java/org/apache/maven/plugins/clean/BackgroundCleaner.java:
##########
@@ -255,18 +382,164 @@ boolean fastDelete(Path baseDir) throws IOException {
}
/**
- * Deletes the given directory without logging messages and without
throwing {@link IOException}.
- * The exceptions are stored for reporting after the end of the session.
+ * Deletes the given directory in a background thread using batch retry.
+ * Unlike the foreground {@link Cleaner}, this method does not call {@code
System.gc()}
+ * or sleep per file, avoiding the stop-the-world JVM pauses that caused
the performance
+ * regression described in MCLEAN-102.
+ *
+ * <p>The deletion proceeds in two passes:</p>
+ * <ol>
+ * <li><b>Walk:</b> traverse the file tree and attempt to delete each
file/directory once.
+ * Failures are silently collected without retrying.</li>
+ * <li><b>Batch retry:</b> if {@code retryOnError} is enabled and there
were failures,
+ * sleep once ({@value #BATCH_RETRY_DELAY_MS}ms) to let external
processes release
+ * file locks, then retry all failures together.</li>
+ * </ol>
+ *
+ * <p>Any files that still cannot be deleted after the batch retry will be
cleaned up
+ * by the {@linkplain #scanForLeftovers() leftover scan} on the next
build.</p>
*
* <h4>Thread safety</h4>
- * Contrarily to most other methods in {@code BackgroundCleaner}, this
method is
- * thread-safe because it uses a copy of this cleaner for walking in the
file tree.
+ * This method is designed to run in the background executor thread. It
does not share
+ * any mutable state with the main thread except through {@link
#errorOccurred(IOException)},
+ * which is synchronized.
+ *
+ * @param dir the directory to delete
+ * @param force whether to force the deletion of read-only files
+ * @param retryOnError whether to undertake a batch retry of failed
deletions
*/
- private void deleteSilently(final Path dir) {
+ private void deleteInBackground(Path dir, boolean force, boolean
retryOnError) {
+ logger.debug("Deleting " + dir + " in background.");
+ List<Path> failures = new ArrayList<>();
try {
- Files.walkFileTree(dir, Set.of(), Integer.MAX_VALUE, new
Cleaner(this));
+ Files.walkFileTree(dir, Set.of(), Integer.MAX_VALUE, new
SimpleFileVisitor<>() {
+ /**
+ * Current depth relative to the root directory, used by
{@link Cleaner#setWritable}
+ * to walk up to the parent when the file itself is already
writable.
+ */
+ int depth;
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path d,
BasicFileAttributes attrs) {
+ if (ON_WINDOWS && attrs.isOther()) {
+ // MCLEAN-93: NTFS junctions have isDirectory() and
isOther() attributes set.
+ // Delete the junction itself and skip its contents to
avoid deleting the
+ // contents of the junction target, which may be
outside the project.
+ if (!tryDeleteOnce(d, force, depth)) {
+ failures.add(d);
+ }
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ depth++;
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) {
+ if (!tryDeleteOnce(file, force, depth)) {
+ failures.add(file);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path d, IOException
exc) {
+ depth--;
+ if (!tryDeleteOnce(d, force, depth)) {
+ failures.add(d);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException
exc) {
+ failures.add(file);
+ return FileVisitResult.CONTINUE;
+ }
+ });
} catch (IOException e) {
errorOccurred(e);
+ return;
+ }
+ if (!failures.isEmpty() && retryOnError) {
+ try {
+ Thread.sleep(BATCH_RETRY_DELAY_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // Report collected failures before returning.
+ errorOccurred(buildFailureException(failures));
+ return;
+ }
+ List<Path> remaining = new ArrayList<>();
+ for (Path path : failures) {
+ if (!tryDeleteOnce(path, force, 0)) {
Review Comment:
Minor consistency point, not a bug report: the three walk-phase calls now
pass the real depth, but this one still passes a hard-coded `0`.
I tried to build a case where it changes the outcome and could not:
- With `force=true`, any path that reached `failures` via a walk-phase
attempt already had the full `setWritable` loop applied at its correct depth,
so by retry time the permissions are as fixed as they are going to get and the
depth no longer matters.
- The only paths that arrive here without a walk-phase attempt are the ones
`visitFileFailed` adds. For those the retry fails with
`DirectoryNotEmptyException` rather than `AccessDeniedException`, so the
`force` branch is never entered and the depth is again irrelevant.
So I am not claiming a defect — treat this purely as tidiness. The reason I
would still change it: the literal `0` is the exact shape of the bug that was
just fixed in the three calls above, and anyone extending this loop later (for
instance queueing the walk-phase depth along with the path, which the reporting
discussion in the other thread would want anyway) inherits a silently wrong
argument. Carrying the depth with each queued path, or at minimum a comment
saying why `0` is safe here, would close the gap.
Entirely optional, and fine to skip.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]