Skip to content

Prevent duplicate class inclusion in runtime artifacts#11853

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 5 commits into
masterfrom
bdu/avoid-duplicate-classes
Jul 3, 2026
Merged

Prevent duplicate class inclusion in runtime artifacts#11853
gh-worker-dd-mergequeue-cf854d[bot] merged 5 commits into
masterfrom
bdu/avoid-duplicate-classes

Conversation

@bric3

@bric3 bric3 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

This pull request resolves issues with duplicate class inclusion in runtime artifacts of various components:

  • Ensures additional source-set outputs added by TracerJavaExtension.addSourceSetFor are kept local to compile and test classpaths.
  • The relocated jakarta-servlet-5.0 jar is now excluded from runtimeElements and kept local to the producer module's compile and test classpaths.
  • Prevents publishing Java-version-specific source-set outputs in the agent-installer project and ensures its runtime artifacts remain complete.

These changes eliminate duplicate classes in consumer projects while maintaining existing artifact contracts.

Note

Now, duplicate classes fail the build of the instrumentation shadow jar.

The basics of this change is to remove the declaration of the additional source set output to the implementation configuration. And instead make the classes available to the more precise configuration compileOnly and testImplementation.

The additional classes are still included in the project's jar artifact.

The reason is that when a project like ...:instrumentation consumes another project as a dependency to be on its classpath, Gradle resolves the runtimeElements _consumable configuration of the project dependency. And runtimeElements contains by default the project jar artifact and runtime dependencies that were added to the implementation configuration. Breaking this link solves the class duplication.

Motivation

The duplicates being published in runtime dependencies were causing redundant class loading downstream, especially in instrumentation projects, and requiring workarounds in the intrumentation shadow jar. While projects themselves should have proper configuration.

This PR ensures cleaner dependency resolution and eliminates the need for exclusions in shadow jars.

Additional Notes

Follow-up to

Kept the "jar contract", i.e. it's the jar that is being consumed that should have the classes form the extra source sets.

At first, I considered removing the jar.from(additionalSourceSet.output) config, it would remove the duplicate, but it would make the consumed module jar incomplete and leave consuming projects relying on class directories instead of the artifact. This would also have the side effect of making Gradle to track individual classes for up-to-date tracking, which consumes more memory (the doc is on a related topic, but it would have applied here as well).

I ran this command, and it reported no changes in the final jar.

$ jardiff -c classdata --include '**/*.class,**/*.classdata' \
    ../master-273405/dd-java-agent/build/libs/dd-java-agent-1.64.0-SNAPSHOT.jar \
    dd-java-agent/build/libs/dd-java-agent-1.64.0-SNAPSHOT.jar

Contributor Checklist

  • Format the title according to the contribution guidelines
  • Assign the type: and (comp: or inst:) labels in addition to any other useful labels
  • Avoid using close, fix, or any linking keywords when referencing an issue
    Use solves instead, and assign the PR milestone to the issue
  • Update the CODEOWNERS file on source file addition, migration, or deletion
  • Update public documentation with any new configuration flags or behaviors
  • Add your completed PR to the merge queue by commenting /merge. You can also:
    • Customize the commit message associated with the merge with /merge --commit-message "..."
    • Remove your PR from the merge queue with /merge -c
    • Skip all merge queue checks with /merge -f --reason "reason"; please use this judiciously, as some checks do not run at the PR-level (note: the PR still needs to be mergeable, this will only skip the pre-merge build)
    • Get more information in this doc

Jira ticket: [PROJ-IDENT]

bric3 added 4 commits July 3, 2026 15:42
The `TracerJavaExtension.addSourceSetFor` added the extra source-set
output to the `implementation` configuration.

A project dependency on the runtime classpath resolves the producer's
`runtimeElements` variant. That variant publishes the project jar and
runtime dependencies derived from `implementation`, so consumers
(the instrumentation project in particular) received the same classes
twice:

1. once from the jar configured with
   `jar.from(mainForJavaVersionSourceSet.output)`,
2. once from the raw class directory published as a dependency

This change keeps the additional source set local to the producer
module's _compile_ and _test_ classpaths respectively with
`compileOnly` and `testImplementation`.
The jar remains the runtime artifact consumed by downstream aggregating
projects, and it still contains the additional source-set classes.

What was considered: dropping the `jar.from(output)` config, it would
remove the duplicate, but it would make the consumed module jar
incomplete and leave consuming projects relying on class directories
instead of the artifact. This would also a the side effect of making
Gradle to track individual classes for up-to-date tracking, which
consumes more memory.

This keeps the existing "jar contract" and only prevent publishing
the additional source-set output through `runtimeElements`.

This removes the exception-profiling and instrumentation duplicate
exclusions that were needed in the instrumentation `shadowJar`.
The `agent-installer` project added its Java-version-specific source-set
outputs to the `runtimeOnly` configuration.

A project dependency on the runtime classpath resolves the producer's
`runtimeElements` variant. That variant publishes the project jar and
runtime dependencies derived from `runtimeOnly`, so consumers
(the instrumentation project in particular) received the same classes
twice:

1. once from the jar configured with
   `from sourceSets.main_java11.output` and
   `from sourceSets.main_java25.output`,
2. once from the raw class directories published as runtime dependencies

This change keeps the Java-version-specific source sets local to the
producer module's test classpath with `testImplementation`.
The jar remains the runtime artifact consumed by downstream aggregating
projects, and it still contains the Java 11 and Java 25 source-set
classes.

Dropping the jar configuration would remove the duplicate, but it would
make the consumed `agent-installer` jar incomplete and leave consuming
projects relying on class directories instead of the artifact.

This keeps the existing "jar contract" and only prevents publishing
the Java-version-specific source-set output through `runtimeElements`.

This removes the agent-installer duplicate exclusions that were needed
in the instrumentation `shadowJar`.
The `jakarta-servlet-5.0` project added the relocated javax-to-jakarta
advice jar to the `implementation` configuration. And a project
dependency on the runtime classpath resolves the producer's
`runtimeElements` variant. That variant publishes the project jar and
its runtime dependencies that are derived from `implementation`, so
consumers (the instrumentation project in particular) received the same
classes twice:

1. once from the module jar configured with
   `from zipTree(relocatedJavaxJar.outputs.files.asPath)`,
2. once from the relocated jar published as a runtime dependency

This change keeps the relocated jar local to the producer module's
_compile_ and _test_ classpaths with `compileOnly` and
`testImplementation` respectively.

The module jar remains the runtime artifact consumed by downstream
projects, and it still contains the relocated servlet5 advice classes.

With this, the last duplicate is gone, so drop the remaining
`filesMatching { EXCLUDE }` workaround and refresh the comment.
@bric3 bric3 requested review from a team as code owners July 3, 2026 13:48
@bric3 bric3 requested review from ValentinZakharov and removed request for a team July 3, 2026 13:48
@bric3 bric3 added tag: no release notes Changes to exclude from release notes comp: tooling Build & Tooling labels Jul 3, 2026
@bric3 bric3 requested a review from mcculls July 3, 2026 13:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: adcf141c62

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Publishing the relocated jar there would expose the same servlet5 advice classes
// twice to downstream projects, once from the module jar and once from the
// relocated jar.
compileOnly files(relocatedJavaxJarFile)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep relocated servlet helpers on the muzzle classpath

In the jakarta-servlet-5.0 project, putting relocatedJavaxJarFile only on compileOnly/testImplementation drops it from sourceSets.main.runtimeClasspath. The muzzle task builds its instrumentation loader from allMainSourceSet.runtimeClasspath in MuzzleTask.createAgentClassPath, and MuzzleVersionScanPlugin then locates every helperClassNames() entry for helper injection. Servlet5RequestBodyInstrumentation lists BufferedReaderWrapper, AbstractServletInputStreamWrapper, and Servlet31InputStreamWrapper, which are produced only by relocatedJavaxJar; when running :dd-java-agent:instrumentation:servlet:jakarta-servlet-5.0:muzzle, those helpers are no longer visible even though they are later unpacked into the jar.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note this doesn't affect the content of the final build, but does mean that

./gradlew :dd-java-agent:instrumentation:servlet:jakarta-servlet-5.0:muzzle

currently fails - this just needs fixing before merge

@bric3 bric3 Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes I didn't account muzzle for jakarta-servlet, since it was working for the other instrumentations. I'll look at how to fix that.

@bric3 bric3 Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in f873f22

I made the muzzle task able to be provided an extra classpath. I evaluated another approach by feeding the relocated jar into the runtimeClasspath, but I think this wrong and could be confusing.

Re-ran jardiff on the jar produced by this commit

$ jardiff -c classdata --include '**/*.class,**/*.classdata' \
  ../master-273405/dd-java-agent/build/libs/dd-java-agent-1.64.0-SNAPSHOT.jar \
  dd-java-agent/dd-java-agent/build/libs/dd-java-agent-1.64.0-SNAPSHOT.jar

@datadog-datadog-prod-us1-2

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.96 s 13.90 s [-0.3%; +1.2%] (no difference)
startup:insecure-bank:tracing:Agent 12.91 s 13.02 s [-1.3%; -0.3%] (maybe better)
startup:petclinic:appsec:Agent 16.79 s 17.14 s [-6.2%; +2.1%] (no difference)
startup:petclinic:iast:Agent 16.88 s 17.39 s [-7.2%; +1.4%] (no difference)
startup:petclinic:profiling:Agent 17.46 s 17.41 s [-1.2%; +1.7%] (no difference)
startup:petclinic:sca:Agent 16.90 s 17.43 s [-7.2%; +1.1%] (no difference)
startup:petclinic:tracing:Agent 16.56 s 16.69 s [-1.7%; +0.2%] (no difference)

Commit: f873f22d · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

…path

Teach `MuzzleTask` to include an extra classpath (if provided). The goal
is to allow it to see the relocated servlet5 jar.

It is done by introducing `extraAgentClasspath` on the `MuzzelTask`.

Another approach would have been to include the relcated jar on the
runtimeClasspath: `sourceSets.main.runtimeClasspath += files(relocatedJavaxJarFile)`
However this approach creates confusion ; anything that consume this
configuration would see the relocated jar (e.g. during debug).
@bric3

bric3 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 3, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-03 16:30:49 UTC ℹ️ Start processing command /merge


2026-07-03 16:30:54 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-07-03 17:32:58 UTC ℹ️ MergeQueue: This merge request was merged

@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot merged commit 2068d0d into master Jul 3, 2026
587 checks passed
@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot deleted the bdu/avoid-duplicate-classes branch July 3, 2026 17:32
@github-actions github-actions Bot added this to the 1.64.0 milestone Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: tooling Build & Tooling tag: no release notes Changes to exclude from release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants