Skip to content

Separate rewrite & recipe classpath, reload after changes - #463

Open
freya022 wants to merge 10 commits into
openrewrite:mainfrom
freya022:separate-recipe-classpath-plus-support-reload
Open

Separate rewrite & recipe classpath, reload after changes#463
freya022 wants to merge 10 commits into
openrewrite:mainfrom
freya022:separate-recipe-classpath-plus-support-reload

Conversation

@freya022

@freya022 freya022 commented Aug 2, 2026

Copy link
Copy Markdown

What's changed?

  • Followup of Reload recipe classes when a classpath entry changed #456 which, in addition, creates a ClassLoader from the known rewrite dependencies, separated from the dependencies of the rewrite configuration (a recipe's dependencies).

  • The "rewrite" classpath consists of the known rewrite dependencies, but with their effective version.

  • The "recipe" classpath consists of the rewrite Gradle configuration, excluding dependencies present in the "rewrite" classpath

What's your motivation?

  • Fixing Java recipes are not getting updated #453 while also reducing the need for garbage collection of old classes and recompilation of new ones during development. This PR does not change anything outside development scenarios.

Anything in particular you'd like reviewers to focus on?

  • The RewritePluginTest interface was not changed, it was simply moved to IRewritePluginTest but Git fails to see it.
  • I've added a test in (the new) RewritePluginTest to make sure that the effective classpath (how it was resolved before this PR) is the same as the combination of both rewrite + recipe classpaths, I've written the assertions inside the test's build script as using ProjectBuilder worked but was terribly inefficient/slow, let me know if there is a better way to test this

Any additional context

The previous PR mentioned there were issues with class loader leaks, however:

  • When a recipe (from JDA) was updated using the previous PR, this resulted in ~8K classes being loaded and 52MB of metaspace being taken
  • Some steps had to be taken to remove references to the old RewriteClassLoader, the changes did eliminate the leaking references, but the GC was quite lazy in reclaiming the memory, although the loader was correctly marked as reclaimable.
  • When switching the GC to Shenandoah, all class loaders were reclaimed immediately upon a full GC, their classes unloaded and the metaspace shrunk.
    Note that this is not a requirement, it is useful for testing, but G1 still reclaims memory in response to pressure, in other words, its not entirely broken, just very lazy
  • Regardless of the PR (yours or this followup) or the GC being used, I have run the test suite with the custom gradle.properties removed, now using the default memory settings, and could not observe an OOM, unlike with the main branch which held multiple (>6) leaked class loaders

With these new changes, rewriteDryRun after a recipe update runs much faster compared to the previous PR, as most of rewrite's classes are reused, and only a fraction of the classes are reloaded, in my case, ~700 classes are loaded while taking 2.6 MB of metaspace (compared to ~8K and 52 MB)

I will also note that, sometimes it is possible that the GC gets so lazy that Gradle will assume the Metaspace to be full before the JVM had a chance of cleaning it up, but it does not make the build fail - only the JVM will throw an OOM if there is an actual leakage - it will only make Gradle shut down after the build is finished.

timtebeek and others added 8 commits August 2, 2026 17:35
The `RewriteClassLoader` was cached per Gradle daemon and only replaced
when the set of classpath locations changed. Recipes built by the project
itself keep the same location when rebuilt, so their previous classes kept
being used until the daemon was stopped.

Compare the size and last modified time of each classpath entry as well,
and shut down JGit's per class loader work queue thread when a class loader
is replaced, as that thread would otherwise retain it.
To greatly reduce reloading classes during development by reusing most of rewrite's classes
To make sure the merged classpath is the same as before
So recipes from both the RewriteClassLoader and the recipe's CL can be discovered
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants