Skip to content

Make Config / InstrumenterConfig modifiable via a load-time test agent - #11397

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 8 commits into
masterfrom
bdu/moves-with-config-extension-agent-to-commandline-via-convention-plugin
May 20, 2026
Merged

Make Config / InstrumenterConfig modifiable via a load-time test agent#11397
gh-worker-dd-mergequeue-cf854d[bot] merged 8 commits into
masterfrom
bdu/moves-with-config-extension-agent-to-commandline-via-convention-plugin

Conversation

@bric3

@bric3bric3 commented May 18, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Replaces the runtime ByteBuddy retransformation done by WithConfigExtension with a load-time Java agent that rewrites the INSTANCE field of datadog.trace.api.Config and datadog.trace.api.InstrumenterConfig to be public / static / volatile / non-final before the config classes are first defined.

  • It creates a new convention plugin buildSrc/modifiable-config-agent that applies an agent that modifies the modifiers of INSTANCE for these two config classes for each gradle test tasks. Every module that applies java_no_deps.gradle benefit from it.

  • WithConfigExtension:

    • No longer programmatically installs a ByteBuddy retransformer.
    • A static {} block verifies the agent did its job (ensureConfigInstrumentationHasBeenApplied) and caches the reflective field/constructor.

Supersedes #11388.

Motivation

WithConfigExtension made the singleton config classes mutable by using ByteBuddy retransformation from @BeforeAll. Retransforming a class that has already been loaded and touched is not guaranteed by the JVM — it depends on the vendor, the JVM version, and the redefinition capabilities advertised by the JVMTI environment. Several entry points (CoreTracer, etc.) touch Config or InstrumenterConfig before WithConfigExtension is applied — for example ParentBasedAlwaysOnSamplerTest builds a CoreTracer that accesses InstrumenterConfig without ever activating WithConfigExtension, so on a later test that relies on InstrumenterConfig.INSTANCE being instrumented, the agent failed silently, and was producing an error when the field was modified by the junit extension.

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
at app//WithConfigExtension.checkWritable(WithConfigExtension.java:331)
at app//WithConfigExtension.checkConfigTransformation(WithConfigExtension.java:324)
at app//WithConfigExtension.beforeAll(WithConfigExtension.java:91)
at java.base@11.0.30/ArrayList.forEach(ArrayList.java:1541)
Suppressed: java.lang.AssertionError: Failed to rebuild config
at app//WithConfigExtension.rebuildConfig(WithConfigExtension.java:295)
at app//WithConfigExtension.afterAll(WithConfigExtension.java:129)
Caused by: java.lang.IllegalAccessException: class WithConfigExtension
cannot access a member of class InstrumenterConfig
with modifiers "private static final"
at java.base@11.0.30/Field.set(Field.java:784)
at app//WithConfigExtension.rebuildConfig(WithConfigExtension.java:291)

This shows up in CI as recurring test_base failures on:

FailuresJobVendorVersion
28test_flaky: [:baseTest, 3/4]Temurin (HotSpot)1.8.0_482
18test_base: [ibm8, 3/4]IBM J91.8.0_491
15test_base: [semeru17, 3/4]OpenJ9 / Semeru17.0.18
12test_base: [semeru8, 3/4]OpenJ9 / Semeru1.8.0_482
6test_base: [semeru11, 3/4]OpenJ9 / Semeru11.0.30

Instead with a load-time agent that intercepts the class before it is defined, prevents scenarios were the config classes are loaded before WithConfigExtension.

Note

Regarding the PendingTraceBufferTest#bufferFullYieldsImmediateWrite, it was found that on IBM J9 1.8 the test case fixture is actually slower than other JVMs, the buffer capacity is 4096, and this loop can take ~8s, we found that it was ok to just bump the timeout for this method.

 // Fill the buffer
for (int i = 0; i < capacity; i++) {
addContinuation(newSpanOf(factory.create(DDTraceId.ONE))).finish();
}

Additional Notes

  • Gradle's Test task already runs its tests in a separate JVM, so attaching -javaagent do not create an addition JVM. Note the ForkedTest (means forkEvery = 1, that is for each test case a new JVM is created).
  • Tests already running without WithConfigExtension: the agent rewrites the fields regardless of whether the test uses the extension. So tests like ParentBasedAlwaysOnSamplerTest now run against a modifiable config, at build time the field remains final.
  • WithConfigExtension fails early if for some reason the config instance or not "writable", and points to the agent / gradle convention plugin being not setup.
  • The agent jar follows the same build structure than call-site-instrumentation-plugin, however it's not the best practice here and a follow might be needed.

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
  • Update the CODEOWNERS file — N/A, no ownership move
  • Update public documentation — N/A, internal test infrastructure only

Supersedes #11388.

Jira ticket: [PROJ-IDENT]

bric3and others added 2 commits May 18, 2026 12:30
…agent
The JUnit 5 WithConfigExtension used ByteBuddy retransformation at
@BeforeAll to make Config and InstrumenterConfig INSTANCE fields
public/volatile/non-final. Retransformation is not guaranteed when the
class has been loaded before the extension runs (e.g. through
CoreTracer), which caused intermittent failures on at least HotSpot 1.8,
IBM J9, and OpenJ9 Semeru jobs.
This commit switches to a load-time javaagent that rewrites the INSTANCE
fields upon class cload.
It introduces in particular `dd-trace-java.modifiable-config` convention
plugin whose role is to configure the test jvm with the java agent to
every Test task. This convention plugin is applied automatically via
dd-trace-java.configure-tests which is itself applied via
java_no_deps.gradle.
`WithConfigExtension` no longer installs a ByteBuddy agent
programmatically; instead it just verifies the fields are already
modifiable.
…ntees writable Config fields
With modifiable-config-agent attached to every test JVM, the `INSTANCE`
fields of `Config` and `InstrumenterConfig` are always
`public`/`volatile`/non-`final`
by the time `WithConfigExtension` is loaded. The runtime state
machine that previously guarded the ByteBuddy retransform is therefore
dead code.
Collapse it:
- Move reflective lookups of the INSTANCE fields and default constructors
into a static {} block that first calls
ensureConfigInstrumentationHasBeenApplied() and then caches the
handles as static finals. Failure throws ExceptionInInitializerError,
which surfaces as a clear test-class load failure pointing at the
Gradle convention plugin.
- Remove the isConfigInstanceModifiable / configModificationFailed /
configTransformerInstalled flags and every guard they fed.
- Drop makeConfigInstanceModifiable(), checkConfigTransformation(),
and checkWritable() — all redundant with the static-init check.
- Drop the corresponding JUnit assert* static imports.
- beforeAll/beforeEach/afterEach/afterAll now call rebuildConfig()
unconditionally.
- Update the class Javadoc to reflect the new mechanism.
Co-Authored-By: Claude <noreply@anthropic.com>
@bric3bric3 added type: bug fix Bug fix comp: tooling Build & Tooling labels May 18, 2026
[ci: NON_DEFAULT_JVMS]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bric3bric3 added tag: ibm IBM JVM related tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes labels May 18, 2026
bric3and others added 3 commits May 18, 2026 13:08
[ci: NON_DEFAULT_JVMS]
…avaagent
The `dd-trace-java.modifiable-config` convention plugin unconditionally
attached a second `-javaagent:modifiable-config-agent.jar` to every Test
JVM. That breaks the few modules whose tests already launch with the real
dd-java-agent attached (currently only `:dd-java-agent`'s integration
tests, via the `doFirst` at `dd-java-agent/build.gradle:454-457`):
* `AgentBootstrap.installAgentJar` tries the class' CodeSource first,
then `getAgentFileFromJavaagentArg`, then a ClassLoader-resource
lookup. On the failing matrix the CodeSource is null for the
bootstrap class, so it falls through to the `-javaagent:` arg.
* `getAgentFileFromJavaagentArg` refuses to pick when more than one
`-javaagent:` is present ("multiple javaagents specified") and
returns null.
* The ClassLoader fallback then throws `IllegalArgumentException: URI
is not absolute` for the bootstrap class URL.
Net effect: the tracing agent never installs and suites like
`OpenTracingTest` / `ShadowPackageRenamingTest` fail across non-default
JVMs (ibm8, semeru11/17, zulu11, tip).
Alternatives considered and rejected:
* Manifest scan in `getAgentFileFromJavaagentArg` to pick the entry
whose `Main-Class` is `AgentBootstrap`: meaningful bootstrap-
performance cost (open every `-javaagent:` jar, read MANIFEST.MF)
on a hot path that runs on every JVM startup.
* `-Ddd.agent.jar.path` / `DD_AGENT_JAR_PATH`: no equivalent knob
exists today, adds an attacker-controllable file path, and only
helps when the CLI is already ambiguous — not worth the surface.
Skipping is safe for `:dd-java-agent`'s tests: nothing under
`dd-java-agent/src/test` (Java or Groovy) references
`Config` / `InstrumenterConfig` `INSTANCE`, and nothing uses
`WithConfigExtension`. The tests rely on the real agent's startup to
populate `Config.INSTANCE`, not on swapping the singleton mid-test.
Implementation: defer the attach to a `doFirst` that inspects
`allJvmArgs` for an existing non-modifiable-config `-javaagent:` entry.
Gradle prepends each new `doFirst`, so the plugin's check runs after
the project's own agent-attach `doFirst` and sees the real arg before
the JVM forks. When skipping, emit an `INFO`-level log so the decision
is visible under `--info`.
[ci: NON_DEFAULT_JVMS]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bric3
bric3force-pushed the bdu/moves-with-config-extension-agent-to-commandline-via-convention-plugin branch from 6d3aea1 to 9a48fb5CompareMay 18, 2026 12:21
@bric3bric3 changed the title Make Config / InstrumenterConfig modifiable via a load-time test agentMake Config / InstrumenterConfig modifiable via a load-time test agentMay 18, 2026
@jpbempel
jpbempelforce-pushed the bdu/moves-with-config-extension-agent-to-commandline-via-convention-plugin branch 7 times, most recently from b2ec041 to 4d1114eCompareMay 19, 2026 08:55
filling the delaying buffer for test bufferFullYieldsImmediateWrite
takes more than 5 seconds on ibm8 and CI
[ci: NON_DEFAULT_JVMS]
@jpbempel
jpbempelforce-pushed the bdu/moves-with-config-extension-agent-to-commandline-via-convention-plugin branch from 4d1114e to eb901d7CompareMay 19, 2026 08:56
@jpbempel
jpbempel marked this pull request as ready for review May 19, 2026 11:05
@jpbempel
jpbempel requested a review from a team as a code ownerMay 19, 2026 11:05
@jpbempel
jpbempel requested a review from mccullsMay 19, 2026 11:05

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:eb901d7c1e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +29 to +32
val foreignAgent = allJvmArgs.firstOrNull {
it.startsWith("-javaagent:") && !it.contains("modifiable-config-agent")
}
if (foreignAgent != null) {

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 Always attach the config agent alongside dd-java-agent

When a test task already adds the Datadog agent, this branch skips the only transformer that now makes Config/InstrumenterConfig.INSTANCE writable. I checked dd-java-agent/build.gradle lines 454-456, where every dd-java-agent test task adds -javaagent:${shadowJar...} in doFirst, and there are many @WithConfig JUnit tests under dd-java-agent/instrumentation; those tests will load WithConfigExtension, hit its new static check, and fail because the modifiable-config agent was deliberately not attached. The JVM supports multiple -javaagent arguments, so this should not skip just because another agent is present.

Useful? React with 👍 / 👎.

@datadog-datadog-prod-us1-2

This comment has been minimized.

@bric3
bric3 enabled auto-merge May 19, 2026 18:37
@bric3
bric3 added this pull request to the merge queueMay 20, 2026
@dd-octo-sts

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351Bot commented May 20, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-05-20 05:23:12 UTC ℹ️ Start processing command /merge


2026-05-20 05:23:17 UTC ℹ️ MergeQueue: pull request added to the queue

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


2026-05-20 06:33:40 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks May 20, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot merged commit 5181a21 into masterMay 20, 2026
571 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot deleted the bdu/moves-with-config-extension-agent-to-commandline-via-convention-plugin branch May 20, 2026 06:33
@github-actionsgithub-actionsBot added this to the 1.63.0 milestone May 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: toolingBuild & Toolingtag: ai generatedLargely based on code generated by an AI or LLMtag: ibmIBM JVM relatedtag: no release notesChanges to exclude from release notestype: bug fixBug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bric3@jpbempel