Skip to content

Make plugin @allocates estimator classes linkable - #158779

Draft
jdconrad wants to merge 5 commits into
elastic:mainfrom
jdconrad:painless-alloc-plugin-estimator-linkage
Draft

Make plugin @allocates estimator classes linkable#158779
jdconrad wants to merge 5 commits into
elastic:mainfrom
jdconrad:painless-alloc-plugin-estimator-linkage

Conversation

@jdconrad

@jdconrad jdconrad commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Enabling allocation tracking on serverless QA broke every script calling ml's domainSplit with a NoClassDefFoundError for MlAllocationEstimators.

The annotation resolves correctly at allowlist-load time, because resolveAllocationEstimator uses the plugin's own class loader. The problem is downstream: the pre-check emits an INVOKESTATIC that names the estimator's class, and bytecode can only reference a class symbolically, by name. At link time the generated script's loader resolves that name through javaClassNamesToClasses, and its parent is lang-painless, not the plugin. The estimator class was never registered there, so the name resolved to nothing. The resolved Method sitting on the PainlessMethod is a compile-time object and is not consulted during linking.

The fix registers the class where it is resolved. That map exists for precisely this case — its own comment describes it as "all the classes that need to be available to the custom classloader ... but not necessarily whitelisted individually" — and the from_class target on the very same allowlist line is already registered the same way by addImportedPainlessMethod. Registering grants linkage only; script-visible types come from canonicalClassNamesToClasses, which this does not touch.

All five plugin estimator classes were affected: ml, sql, eql, mapper-version and mapper-unsigned-long. Built-in estimators were never affected because they live in lang-painless, the parent loader. The def path was also unaffected, since it unreflects the estimator into a MethodHandle that holds a direct reference and never resolves a name — which is why the tested path worked and the emitted path did not.

Worth knowing for review: a unit test cannot reproduce this failure. Tests run on one flat classpath, so the parent loader resolves everything and linkage always succeeds; that is why it got through. The test therefore asserts the registration itself, which does fail without the fix, plus that the class gains no script-visible surface — not nameable by short or qualified name, not resolvable as a type, absent from the _context API, and with no PainlessClass so no callable members.

Enabling allocation tracking on serverless QA broke every script
calling ml's domainSplit with a NoClassDefFoundError for
MlAllocationEstimators.

The annotation resolves fine at allowlist-load time, using the
plugin's own class loader. But the pre-check emits an INVOKESTATIC
that names the estimator's class, and bytecode can only reference a
class by name. At link time the generated script's loader resolves
that name through javaClassNamesToClasses, whose parent is
lang-painless, not the plugin. The estimator was never registered
there, so the name resolved to nothing.

Register it where it is resolved. That map exists for exactly this:
classes the generated code must link to but scripts must not see.
The from_class target on the same allowlist line is already
registered the same way.

This affected all five plugin estimator classes, in ml, sql, eql,
mapper-version and mapper-unsigned-long. Built-in estimators were
unaffected because they live in lang-painless, the parent loader.
The def path was unaffected too: it unreflects the estimator into a
MethodHandle, which holds a direct reference and never resolves a
name.

A unit test cannot reproduce the failure, since one flat classpath
means the parent loader resolves everything, which is why this got
through. The test asserts the registration instead, and that the
class stays unnameable, untyped, unlisted and memberless.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🔍 Preview links for changed docs

⏳ Building and deploying preview... View progress

This comment will be updated with preview links when the build is complete.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Important: Docs version tagging

👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version.

We use applies_to tags to mark version-specific features and changes.

Expand for a quick overview

When to use applies_to tags:

✅ At the page level to indicate which products/deployments the content applies to (mandatory)
✅ When features change state (e.g. preview, ga) in a specific version
✅ When availability differs across deployments and environments

What NOT to do:

❌ Don't remove or replace information that applies to an older version
❌ Don't add new information that applies to a specific version without an applies_to tag
❌ Don't forget that applies_to tags can be used at the page, section, and inline level

🤔 Need help?

A unit test cannot reach this failure: tests run on one flat
classpath, so the generated script's parent loader resolves every
estimator and linkage always succeeds. That is why the ml breakage
reached serverless QA.

The example plugin is a real plugin with its own class loader, so
it does reproduce it. Adds an estimator class there, an allocating
method annotated to use it, and the system property that makes the
pre-check emit the call. Verified by reverting the fix: the script
then fails with no_class_def_found_error naming the estimator,
which is the QA failure exactly.

The estimator class is deliberately not allowlisted, matching how
the x-pack modules ship theirs, and a second case checks scripts
still cannot name it.

This doubles as the missing worked example of @allocates for plugin
authors, which the example plugin had nothing on.
The pre-flight compile gate derives a compile task from every
changed test file, but plugins/examples is its own Gradle build, so
:plugins:examples:...:compileYamlRestTestJava does not resolve and
the gate fails. Any PR touching a test there hits this, and because
the gate is shared, every flakiness re-run is skipped with it.

Skip those paths. The root build cannot run those tests anyway;
they are covered by the example-plugins periodic job.
A def call resolves its target at runtime and charges through a
MethodHandle, which holds a direct class reference and never
resolves a name, so it works with or without the linkage fix. The
emitted call is the one that broke. Covering both routes means
neither can regress on its own.

Adds a static allowlisted method too, whose estimator takes only
the arguments rather than the receiver first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants