Skip to content

Share artifact cache across JavaSourceSetUpdater visitors#7454

Merged
steve-aom-elliott merged 2 commits intomainfrom
share-javasourceset-artifact-cache
Apr 22, 2026
Merged

Share artifact cache across JavaSourceSetUpdater visitors#7454
steve-aom-elliott merged 2 commits intomainfrom
share-javasourceset-artifact-cache

Conversation

@steve-aom-elliott
Copy link
Copy Markdown
Contributor

@steve-aom-elliott steve-aom-elliott commented Apr 22, 2026

What

JavaSourceSetUpdater (introduced in #7202, refined by #7358 / #7373 / #7376) downloads each newly-added or changed dependency's JAR so it can refresh the in-memory JavaSourceSet marker with the new types. Two pieces of state were not shared across visitors:

  1. The artifact cache. The constructor allocated a fresh Files.createTempDirectory("rewrite-artifact-cache") and wrapped it in a LocalMavenArtifactCache per visitor instance, so the same JARs were re-downloaded across recipes in a run.
  2. The result of scanning the JAR. JavaSourceSet.typesFromPath(jarPath, null) re-opens the JAR and re-walks its entries on every call, even though the result is a pure function of the GAV's bytes.

Every dependency-mutating recipe (AddDependency, UpgradeDependencyVersion, ChangeDependency, ChangeDependencyGroupIdAndArtifactId in both rewrite-maven and rewrite-gradle) constructs its own updater, so both costs were paid per recipe per source set.

This PR shares both, scoped to the ExecutionContext.

Why

Migration recipe chains (e.g. Spring Boot 3 → 4) trigger dozens of dependency-mutating recipes per repository. Without sharing, the same JARs are re-downloaded and re-scanned across the run — multiplying outbound traffic to Maven Central / artifactory and burning CPU on redundant JAR walks.

How

1. Shared MavenArtifactCache on MavenExecutionContextView

Mirrors the existing MavenPomCache pattern:

  • New MAVEN_ARTIFACT_CACHE message key.
  • New setArtifactCache(MavenArtifactCache) setter for embedders that want to override (e.g. point at a persistent volume).
  • New getArtifactCache() that lazily defaults to a LocalMavenArtifactCache at ~/.rewrite/cache/artifacts (the same directory JavaRewriteRpc already uses), falling back to a single JVM-lifetime temp directory if the user home isn't writable.
  • JavaSourceSetUpdater constructor now resolves its downloader's cache from mctx.getArtifactCache() instead of allocating a per-visitor temp dir.

2. Memoized typesFromPath results on the ExecutionContext

  • JavaSourceSetUpdater reads/writes a Map<String, List<JavaType.FullyQualified>> stored on the ctx under "org.openrewrite.maven.jarTypeCache", keyed by groupId:artifactId:version.
  • Only positive results are cached. addDependency tries each repository in order until one succeeds, so caching empty results would short-circuit later repos.

Tests

New MavenExecutionContextViewTest:

  • artifactCacheIsMemoizedAcrossCalls — repeated getArtifactCache() calls return the same instance.
  • setArtifactCacheOverridesDefaultsetArtifactCache(...) overrides the lazy default.
  • artifactCacheIsSharedBetweenViewsOverSameDelegate — load-bearing: each JavaSourceSetUpdater calls MavenExecutionContextView.view(ctx) freshly, so the cache must live on the underlying delegate's message map, not on the view.

New JavaSourceSetUpdaterTest:

  • typeCacheIsSharedAcrossUpdaters — two updaters built from the same ctx see the same type-cache map.

Existing tests still pass: ChangeDependencyGroupIdAndArtifactIdTest (full class, including updatesJavaSourceSetMarkerOnJavaFiles), AddDependencyTest and UpgradeDependencyVersionTest in both rewrite-maven and rewrite-gradle, and ChangeDependencyTest in rewrite-gradle.

JavaSourceSetUpdater previously allocated a fresh temp directory and a fresh
LocalMavenArtifactCache in its constructor. Every recipe visitor that mutates
a dependency (AddDependency, UpgradeDependencyVersion, ChangeDependency,
ChangeDependencyGroupIdAndArtifactId in both rewrite-maven and rewrite-gradle)
constructs its own updater, so the cache was never reused across visitors —
each downloaded the same JARs from scratch.

Add a getArtifactCache()/setArtifactCache() pair on MavenExecutionContextView
mirroring the existing getPomCache()/setPomCache() pattern, lazily defaulting
to a LocalMavenArtifactCache at ~/.rewrite/cache/artifacts (the same path
JavaRewriteRpc already uses) and falling back to a single JVM-lifetime temp
directory if the user home is not writable.

JavaSourceSetUpdater now resolves its downloader cache from this shared cache,
so all updaters built from the same ExecutionContext share artifact bytes.
Embedders that want to point the cache at a persistent or shared volume can
override it via setArtifactCache before any visitor runs.
Even with the artifact cache shared, every visitor that calls
JavaSourceSet.typesFromPath re-opens the JAR and re-walks its entries to
build the FullyQualified type list. The result is a pure function of the
GAV's bytes, so it can be cached by GAV for the lifetime of the
ExecutionContext.

Add a Map<String, List<JavaType.FullyQualified>> on the ctx (keyed by
"org.openrewrite.maven.jarTypeCache") that JavaSourceSetUpdater consults
before scanning. Only positive results are cached: addDependency tries
each repository in order until one succeeds, so caching empty results
would short-circuit later repos.
@steve-aom-elliott steve-aom-elliott added enhancement New feature or request test provided Already replicated with a unit test, using JUnit pioneer's ExpectedToFail maven gradle labels Apr 22, 2026
@steve-aom-elliott steve-aom-elliott moved this from In Progress to Ready to Review in OpenRewrite Apr 22, 2026
@steve-aom-elliott steve-aom-elliott merged commit 8204153 into main Apr 22, 2026
1 check passed
@steve-aom-elliott steve-aom-elliott deleted the share-javasourceset-artifact-cache branch April 22, 2026 19:07
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request gradle maven test provided Already replicated with a unit test, using JUnit pioneer's ExpectedToFail

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant