Refactor fast clean: fix MCLEAN-102, session-scoped sharing, per-module config - #328
Refactor fast clean: fix MCLEAN-102, session-scoped sharing, per-module config#328gnodet wants to merge 1 commit into
Conversation
Refactors BackgroundCleaner to fix the MCLEAN-102 performance regression (50% slower on Windows due to System.gc() stop-the-world pauses from the background thread) and three regressions from PR #286 (thread proliferation, listener proliferation, lost leftover cleanup). BackgroundCleaner is now a standalone session-scoped service (no longer extends Cleaner). Each module gets its own Cleaner with per-module configuration and attaches the shared BackgroundCleaner via Cleaner.setBackgroundCleaner(). Per-module force/retryOnError values are passed to fastDelete() and captured per-directory so background deletion respects the originating module's configuration. Key changes: - Session-scoped via SessionData.computeIfAbsent(): one instance, one thread, one listener per session (was one per module) - Batch retry replaces per-file System.gc() + sleep: walk once, sleep once (250ms), retry all failures together - scanForLeftovers() restores cleanup of dirs from killed previous builds - fastDelete() is synchronized for parallel build safety Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e756fb7 to
e681b19
Compare
| /** | ||
| * A cleaner potentially executed by background threads. | ||
| * A session-scoped service that moves directories to a staging area and deletes them in a background thread. | ||
| * A single instance is shared across all modules in a reactor build via {@link SessionData}, |
There was a problem hiding this comment.
Do we mean "sub-projects" (instead of "modules") in Maven 4 terminology?
| * Returns an error message to show to user if the fast delete failed. | ||
| */ | ||
| @Override | ||
| String fastDeleteError(IOException e) { |
There was a problem hiding this comment.
If this method no longer override a method from the parent class, I presume that it can be made private. Then, if this method appears to be invoked in only one place, maybe it can be completely removed and replaced by moving this code inline at the invocation place, if that make the code easier to follow.
| */ | ||
| @Override | ||
| boolean fastDelete(Path baseDir) throws IOException { | ||
| synchronized boolean fastDelete(Path baseDir, boolean force, boolean retryOnError) throws IOException { |
There was a problem hiding this comment.
If this method does not override anymore a method of the parent class, try to make this method private.
| * @return whether this method was able to register the background task | ||
| * @throws IOException if an error occurred while preparing the task before execution in a background thread | ||
| */ | ||
| boolean fastDelete(Path baseDir) throws IOException { |
There was a problem hiding this comment.
If BackgroundCleaner no longer extent Cleaner, can this method be made private? If this method is invoked in only one location, should the code be moved there?
| /** | ||
| * Returns an error message to show to user if the fast delete failed. | ||
| */ | ||
| String fastDeleteError(IOException e) { |
There was a problem hiding this comment.
If BackgroundCleaner no longer extent Cleaner, can this method be made private? If this method is invoked in only one location, should the code be moved there?
Summary
Refactors the fast clean (
-Dmaven.clean.fast=true)BackgroundCleanerto fix the MCLEAN-102 performance regression and three regressions introduced by the per-module refactoring in PR #286.What this fixes
System.gc()stop-the-world per file + per-file retry sleeps from background threadBackgroundCleaner, each with its ownExecutorServiceSessionData.computeIfAbsent()session.registerListener(this)init()scan removed in PR #286 when singleton was droppedscanForLeftovers()queues leftover dirs for background deletionDesign
BackgroundCleanerno longer extendsCleaner. It is a standalone session-scoped service that owns only the shared infrastructure (executor, session listener, staging directory, fast mode).Each module creates its own
Cleanerwith per-module configuration (force,retryOnError,followSymlinks, etc.) and attaches the sharedBackgroundCleanerviaCleaner.setBackgroundCleaner(). The per-moduleforce/retryOnErrorvalues are passed tofastDelete()and captured alongside each directory (via aDeferredDeletionrecord for at-end mode) so that background deletion respects the originating module's configuration — even in multi-module builds where modules configure the clean plugin differently.Changes
BackgroundCleaner.java(major rewrite):Cleaner— standalone session-scoped serviceSessionData.Key<BackgroundCleaner>+getOrCreate()factory — one instance, one thread, one listener per sessionfastDelete(Path, boolean force, boolean retryOnError)— per-module config captured per-calldeleteInBackground()— usesSimpleFileVisitorwith batch retry instead of per-fileSystem.gc()+ sleepDeferredDeletionrecord — captures per-module config for at-end/defer modesscanForLeftovers()— scans fast directory for dirs left by killed previous buildsfastDelete()issynchronizedfor parallel build safety (-T)Cleaner.java:backgroundCleanerfield +setBackgroundCleaner()setterfastDelete()andfastDeleteError()delegate toBackgroundCleaner, passing per-instanceforce/retryOnErrorsetWritable():private static→static(package-private) soBackgroundCleanercan reuse itCleanMojo.java:Cleanerwith that module's configurationfast=true, gets/creates the sharedBackgroundCleanerand attaches itBefore vs After (500-module reactor)
System.gc()calls (Windows)Design notes
Cleaner.tryDelete()keepsSystem.gc()+ per-file retry — it runs in the main thread where it can't cause stop-the-world on other threads, and it may genuinely need to release JVM-held file handles.retryOnError: if user sets-Dmaven.clean.retryOnError=false, no retry happens even in background.fast=truebuild.Test plan
mvn testpasses (15 tests, 0 failures)-T 4Cparallel build to verify session-scoped sharing🤖 Generated with Claude Code