Skip to content

Helper for ConstructingObjectParser for record classes #132106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

prdoyle
Copy link
Contributor

@prdoyle prdoyle commented Jul 29, 2025

Motivation

ConstructingObjectParser requires a builder parameter that accepts a Object[] and constructs the desired object using those as constructor parameters. This leads to boilerplate like this:

    @SuppressWarnings("unchecked")
    static final ConstructingObjectParser<ProfileDocumentUser, Void> PROFILE_DOC_USER_PARSER = new ConstructingObjectParser<>(
        "user_profile_document_user",
        false,
        (args, v) -> new ProfileDocumentUser(
            (String) args[0],
            (List<String>) args[1],
            (Authentication.RealmRef) args[2],
            (String) args[3],
            (String) args[4]
        )
    );

This is awkward when first developing such a record, when you might add, remove, or rearrange the fields often. It's especially hazardous if two of the arguments have the same datatype, as with the fourth and fifth arguments in the example above, because if you reorder those record components and forget to change this lambda expression, the compiler will not detect the mistake.

And the final insult is, it requires @SuppressWarnings("unchecked") if any of the arguments have generic types, like List<String> above, meaning you need to opt out of those warnings entirely.

Change

  • Add a factory method ConstructingObjectParser.forRecord that accepts the record class and returns a parser for that record
  • Change a few cases where we use the boilerplate above, to serve as examples.

@prdoyle prdoyle self-assigned this Jul 29, 2025
@prdoyle prdoyle added the :Core/Infra/Core Core issues without another label label Jul 29, 2025
@prdoyle prdoyle requested a review from a team as a code owner July 29, 2025 19:26
@prdoyle prdoyle added >refactoring auto-backport Automatically create backport pull requests when merged v9.2.0 v8.19.1 labels Jul 29, 2025
@elasticsearchmachine elasticsearchmachine added the Team:Core/Infra Meta label for core/infra team label Jul 29, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra (Team:Core/Infra)

prdoyle and others added 2 commits July 29, 2025 17:18
This seemed helpful, but it probably causes more harm than good by causing
confusing errors when publicLookup doesn't have the required access. Better to
make the caller pass a Lookup object that has the required access.
MethodHandles.Lookup lookup
) {
Function<Object[], R> builder = recordBuilder(recordClass, lookup);
return new ConstructingObjectParser<>(name, ignoreUnknownFields, (args, context) -> builder.apply(args));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(For the future... if this were able to consume the context argument too, there would be a lot more cases where it would apply. The current one applies only to cases where context is ignored, which seems to be roughly half the places where records are used.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, looking into this some more, my own CPS requirements actually need this functionality, so that means this PR is insufficient for my own needs in its current form.

Copy link
Contributor

@mosche mosche left a comment

Choose a reason for hiding this comment

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

Love the idea of improving this, this is awful at the moment!
Looks good, though I suggest to restructure the code a little bit.

I'm not sure how much we have to worry about performance, but if recall right parsing has come up repeatedly in flame graphs. On the other hand, that was mostly chunking I think and none of the converted records look performance critical.

* typically just {@code MethodHandles.lookup()} called by some code that can access the constructor.
* @return a function suitable to use as the {@code builder} argument for one of the constructors of this class.
*/
public static <R extends Record, Context> ConstructingObjectParser<R, Context> forRecord(
Copy link
Contributor

Choose a reason for hiding this comment

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

How about restructuring this a bit for readability and have recordBuilder return the actual builder BiFunction as expected by ConstructingObjectParser using

  • <R extends Record> MethodHandle recordConstructor(Class<R> recordClass, MethodHandles.Lookup lookup) and
  • <R extends Record, Context> BiFunction<Object[], Context, R> recordBuilder(MethodHandle ctor)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I thought the same thing actually. I should do that.

private static Object constructRecord(Object[] args, MethodHandle ctor) {
Object result;
try {
result = ctor.invokeWithArguments(args);
Copy link
Contributor

Choose a reason for hiding this comment

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

Typically this area is rather performance sensitive, not sure how much we have to worry here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering how invoke on an array-spreading method handle (asSpreader(Object[].class, ctorArgs.length)) compares to invokeWithArguments...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The javadocs say invokeWithArguments is equivalent to this:

MethodHandle invoker = MethodHandles.spreadInvoker(this.type(), 0);
Object result = invoker.invokeExact(this, arguments);

I could perhaps do the spreadInvoker ahead of time, leaving only the invokeExact.

@prdoyle
Copy link
Contributor Author

prdoyle commented Jul 31, 2025

Part of the motivation for this is to enable some performance optimization. By hiding the conversion from Object[] to a record, we could in principle make that intermediate Object[] unnecessary, and instantiate the record directly. That would require custom bytecode, but hey, we're not necessarily adverse to that in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Automatically create backport pull requests when merged :Core/Infra/Core Core issues without another label >refactoring Team:Core/Infra Meta label for core/infra team v8.19.1 v9.2.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants