Skip to content

Skip too new versioned directories of multi-release JARs when instrumenting#25648

Merged
bot-gradle merged 5 commits intomasterfrom
ml/25434/skip-too-new-dirs-in-mrjars
Jul 12, 2023
Merged

Skip too new versioned directories of multi-release JARs when instrumenting#25648
bot-gradle merged 5 commits intomasterfrom
ml/25434/skip-too-new-dirs-in-mrjars

Conversation

@mlopatkin
Copy link
Copy Markdown
Member

@mlopatkin mlopatkin commented Jul 6, 2023

ASM library that we use for bytecode instrumentation has an upper limit to the bytecode version it can understand.
When processing multi-release JARs, the restriction caused errors even when running on the supported JVM, because instrumentation was processing all classes inside the JAR regardless of whether it was multi-release or not: e.g. JAR with versioned directories for Java 21 and Java 11 was still problematic even when running on Java 11 JVM, where new version of classes compiled for Java 21 aren't actually used.

This PR rewrites the instrumentation code to skip all versioned directories which aren't supported. The resulting instrumented JARs only contain versioned directories up to latest ASM-supported Java version. This way the cached transformed JAR is independent of the version of the JVM running Gradle.

However, there's a hole left when the build runs on the not-yet-supported-by-asm (and, therefore, Gradle) JVM, as it can then load non-instrumented classes from the versioned directories of the original JARs. This is going to be addressed in some follow-up PR.

Part of #25434.

@mlopatkin mlopatkin added this to the 8.3 RC1 milestone Jul 6, 2023
@mlopatkin mlopatkin requested review from a team and octylFractal July 6, 2023 16:59
@mlopatkin mlopatkin requested a review from a team as a code owner July 6, 2023 16:59
@mlopatkin mlopatkin self-assigned this Jul 6, 2023
@octylFractal
Copy link
Copy Markdown
Member

I think this should have a test that the MRJAR entries are ignored, though I'm not sure how difficult that would be to add.

@mlopatkin mlopatkin requested a review from a team as a code owner July 10, 2023 14:39
@mlopatkin mlopatkin force-pushed the ml/25434/skip-too-new-dirs-in-mrjars branch from e807519 to 90dca7f Compare July 10, 2023 15:52
@mlopatkin
Copy link
Copy Markdown
Member Author

I think this should have a test that the MRJAR entries are ignored, though I'm not sure how difficult that would be to add.

Thanks, I've added unit test coverage. Setting up a proper integration test is harder, I don't think there is enough ROI.

@mlopatkin mlopatkin changed the base branch from master to release July 11, 2023 17:55
@mlopatkin
Copy link
Copy Markdown
Member Author

@bot-gradle test RfR

@bot-gradle
Copy link
Copy Markdown
Collaborator

I've triggered the following builds for you. Click here to see all build failures.

Copy link
Copy Markdown
Member

@bamboo bamboo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking good overall. I made a few suggestions which could be addressed in a follow-up PR as needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 isSignatureFile(entryName)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Extract complex expression into something like isMultiReleaseJarManifestEntry(entry)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 I wonder if it makes sense to introduce a test to ensure the constant matches the bundled asm 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASM doesn't expose the latest supported Java version anywhere but in the release notes. The closest thing I see it to declare a constant with the ASM version in AsmConstants and have a test that checks it against this one. Failing test may be a stronger nagging towards updating the constant.

I'll file an issue upstream in the ASM project, maybe they expose the info themselves - this would make things easier. For the test, it can be added when addressing ##25649.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 I'd avoid inspecting every entry name by making isMultiReleaseJar a nullable boolean and changing the predicate to isMultiReleaseJar == null && isManifestName(entryName)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid inspecting every entry name by making isMultiReleaseJar a nullable boolean

I think we can even replace the signature file check with manifest check at this point, so we only need to look at the manifest and bail out of the entry for-each immediately. The manifest is typically the first JAR entry, which is nice. But this feels like out of scope for this PR. So far I applied your suggestion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Would it be possible to avoid the check once the manifest has been processed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to avoid the check once the manifest has been processed?

We don't care about manifest here when transforming for the agent. We skip unsupported classes always: if the manifest has the attribute because they are unsupported, and if not - because they cannot be loaded as classes, only as resources. Maybe the comment above is a bit confusing and fails to convey this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Spurious empty line

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 I'd also include the assertContainsFile("Foo.class") assertion here for clarity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 I had to pause to understand the checks multi-release jar manifest so I suggest renaming to something like legacy instrumentation preserves versioned resources in single-release jars (or non multi-release jars or jars without a multi-release attribute).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Any particular reason not to load the manifest on demand?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intended to piggy-back on isManifestPresentAndFirstEntry initially, but discarded. Replaced with a lazy load (and made it possible to manifest even after creating the fixture with checkManifest = false).

@mlopatkin mlopatkin modified the milestones: 8.3 RC1, 8.4 RC1 Jul 12, 2023
@mlopatkin mlopatkin changed the base branch from release to master July 12, 2023 15:28
This commit encapsulates all transformation logic into a separate class
instead of pulling out pieces of it into the policy instance. Policy now
serves as a factory for transformations.

The refactoring paves way to extracting more info from JAR when
preparing the instrumentation strategy.
ASM can only process class files up to a certain version. This can be
problematic for multi-release JARs containing newer classes in the
appropriate versioned directory. Such JARs can be used on a
Gradle-supported JVM, but prior to this change, instrumentation failed
even if the unsupported classes were never loaded.

This commit makes instrumentation skip unsupported versioned
directories. For legacy instrumentation, these directories are filtered
out completely. For agent-based instrumentation, classes of the
unsupported versions are not instrumented and aren't placed into the
transformed archive.

Unfortunately, ASM doesn't provide any kind of constant to specify the
latest supported class file version, so we have to keep track of this
manually.
In some cases JavaVersion class may come from a TAPI client which can be
outdated and lack necessary constants.
@mlopatkin mlopatkin force-pushed the ml/25434/skip-too-new-dirs-in-mrjars branch from 90dca7f to 80994cb Compare July 12, 2023 15:32
@mlopatkin
Copy link
Copy Markdown
Member Author

@bot-gradle test and merge

@gradle gradle deleted a comment from mlopatkin Jul 12, 2023
@bot-gradle
Copy link
Copy Markdown
Collaborator

I've triggered a build for you. Click here to see all failures if there's any.

@bot-gradle bot-gradle merged commit 818a9ae into master Jul 12, 2023
@bot-gradle bot-gradle deleted the ml/25434/skip-too-new-dirs-in-mrjars branch July 12, 2023 17:03
mlopatkin added a commit that referenced this pull request Jan 9, 2024
This is a backport of #25648, but only the legacy instrumentation part.
The agent-based instrumentation is only available in 8+.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Instrumentation of multi-release JARs should not fail on newer versions

4 participants