launchdarkly / java-core

LaunchDarkly monorepo for Java SDK packages
Other
1 stars 3 forks source link

Support for use of java-server-sdk library in GraalVM native image applications #29

Open ketronkowski opened 7 months ago

ketronkowski commented 7 months ago

Is your feature request related to a problem? Please describe.

I currently use the launchdarkly JAVA SDK in Spring (Boot) based application. The library is currently not supported when compiling our application as a native image with GraalVM.

Describe the solution you'd like

I would like for the library to ship its JAR file with some JSON metadata in the META-INF/native-image/... directory, which enables support of dynamic features in the native image.

Describe alternatives you've considered

If it is not acceptable to include the metadata with the library, Oracle has created the graalvm-reachability-repository, which contains this metadata outside of the libraries JAR file. In an ideal world, all of the metadata is moved into the JARs of the libraries, but until our world has reached its ideal state, this repository can will be used.

the advantage of including this metadata with the library is that if your code changes, the metadata can change along with it. Otherwise, users would be broken until the graalvm-reachability-metadata is updated.

Additional context

I have tested the process of generating this metadata for the java-server-sdk library locally following instructions found here and was able to successfully build and run my application natively.

Some of the dynamic features used in the java-server-sdk need some additional configuration to allow it to be used. Please take a look at the following from GraalVM for more info on the dynamic areas

tanderson-ld commented 7 months ago

Thank you @ketronkowski for making this request and letting us know what you're looking for and with ways to achieve support. I'll discuss this with my team and get back to you.

tanderson-ld commented 7 months ago

@ketronkowski we discussed this on team and we don't have any objections to adding this meta info to the project.

Do you know which portions are not reachable?

We do have other priorities at the moment, so practically the best options to get this change integrated is if you submit a PR or provide patch code that we can work off of.

ketronkowski commented 7 months ago

I boiled down all the output you get by generating the native image metadata from your test suite to just the reflect-config.json file with the following classes.

reflect-config.json ``` [ { "name":"com.launchdarkly.sdk.ContextKindTypeAdapter", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.EvaluationReasonTypeAdapter", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.LDValueTypeAdapter", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.json.SdkSerializationExtensions", "methods":[{"name":"getDeserializableClasses","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.BigSegmentStoreWrapper", "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true }, { "name":"com.launchdarkly.sdk.server.DataModel$FeatureFlag", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$FeatureFlag$Migration", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$Prerequisite", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$Rule", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$Segment", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$SegmentTarget", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$Target", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$VariationOrRollout", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModel$WeightedVariation", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModelSerialization$ClauseTypeAdapter", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModelSerialization$RolloutTypeAdapter", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.DataModelSerialization$SegmentRuleTypeAdapter", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.EventBroadcasterImpl", "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true }, { "name":"com.launchdarkly.sdk.server.FeatureFlagsState$FlagMetadata", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.FeatureFlagsState$JsonSerialization", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory", "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.MigrationExecutionFixture", "allDeclaredFields":true, "queryAllDeclaredMethods":true }, { "name":"com.launchdarkly.sdk.server.integrations.FileDataSourceParsing$FlagFileRep", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, { "name":"com.launchdarkly.sdk.server.subsystems.DataStoreTypes$ItemDescriptor", "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] } ] ```

The most involved part for me (and possibly for your team) in making a PR to your repo is making changes to your grade build to generate this data as part of your build process with an up-to-date graalvm toolchain while keeping the backward compatibility that you need to generate the library for.

Possibly the most straightforward, short-term option is for me to make a PR to the graalvm-reachability-repository repository. I would be happy to contribute there as long as I have your support to do that on behalf of your team. I would also be happy to provide your team a patch for that repo directly and have your team make the PR.

ketronkowski commented 7 months ago

I didn't answer your question in my above response.

The initial error we get is when initializing the LDClient(sdkKey). Looks like the problem stems from not being able to instantiate com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory refelctively. There may be follow on reflection errors. I did not try to surface every single one of them. The entire stacktrace is provided below.

com.launchdarkly.sdk.server.StreamProcessor$StreamInputException: com.launchdarkly.sdk.server.subsystems.SerializationException: com.launchdarkly.sdk.server.subsystems.SerializationException: com.launchdarkly.sdk.server.subsystems.SerializationException: java.lang.RuntimeException: Unable to invoke no-args constructor for class com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory. Registering an InstanceCreator with Gson for this type may fix this problem.
    at com.launchdarkly.sdk.server.StreamProcessor.parseStreamJson(StreamProcessor.java:412) ~[na:na]
    at com.launchdarkly.sdk.server.StreamProcessor.handlePut(StreamProcessor.java:338) ~[na:na]
    at com.launchdarkly.sdk.server.StreamProcessor.handleMessage(StreamProcessor.java:272) ~[na:na]
    at com.launchdarkly.sdk.server.StreamProcessor.handleEvent(StreamProcessor.java:261) ~[na:na]
    at com.launchdarkly.sdk.server.StreamProcessor.lambda$start$1(StreamProcessor.java:200) ~[na:na]
    at java.base@21.0.1/java.lang.Thread.runWith(Thread.java:1596) ~[sd-backend-template:na]
    at java.base@21.0.1/java.lang.Thread.run(Thread.java:1583) ~[sd-backend-template:na]
    at org.graalvm.nativeimage.builder/com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:832) ~[sd-backend-template:na]
    at org.graalvm.nativeimage.builder/com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:211) ~[na:na]
Caused by: com.launchdarkly.sdk.server.subsystems.SerializationException: com.launchdarkly.sdk.server.subsystems.SerializationException: com.launchdarkly.sdk.server.subsystems.SerializationException: java.lang.RuntimeException: Unable to invoke no-args constructor for class com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory. Registering an InstanceCreator with Gson for this type may fix this problem.
    at com.launchdarkly.sdk.server.StreamProcessorEvents.parsePutData(StreamProcessorEvents.java:149) ~[na:na]
    at com.launchdarkly.sdk.server.StreamProcessor.parseStreamJson(StreamProcessor.java:407) ~[na:na]
    ... 8 common frames omitted
Caused by: com.launchdarkly.sdk.server.subsystems.SerializationException: com.launchdarkly.sdk.server.subsystems.SerializationException: java.lang.RuntimeException: Unable to invoke no-args constructor for class com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory. Registering an InstanceCreator with Gson for this type may fix this problem.
    at com.launchdarkly.sdk.server.DataModelSerialization.parseFullDataSet(DataModelSerialization.java:152) ~[na:na]
    at com.launchdarkly.sdk.server.StreamProcessorEvents.parsePutData(StreamProcessorEvents.java:133) ~[na:na]
    ... 9 common frames omitted
Caused by: com.launchdarkly.sdk.server.subsystems.SerializationException: java.lang.RuntimeException: Unable to invoke no-args constructor for class com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory. Registering an InstanceCreator with Gson for this type may fix this problem.
    at com.launchdarkly.sdk.server.JsonHelpers.deserialize(JsonHelpers.java:73) ~[na:na]
    at com.launchdarkly.sdk.server.DataModelSerialization.parseFullDataSet(DataModelSerialization.java:136) ~[na:na]
    ... 10 common frames omitted
Caused by: java.lang.RuntimeException: Unable to invoke no-args constructor for class com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory. Registering an InstanceCreator with Gson for this type may fix this problem.
    at com.launchdarkly.shaded.com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:228) ~[na:na]
    at com.launchdarkly.shaded.com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory.getTypeAdapter(JsonAdapterAnnotationTypeAdapterFactory.java:55) ~[na:na]
    at com.launchdarkly.shaded.com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory.create(JsonAdapterAnnotationTypeAdapterFactory.java:49) ~[na:na]
    at com.launchdarkly.shaded.com.google.gson.Gson.getAdapter(Gson.java:489) ~[sd-backend-template:7.1.1]
    at com.launchdarkly.shaded.com.google.gson.Gson.fromJson(Gson.java:962) ~[sd-backend-template:7.1.1]
    at com.launchdarkly.sdk.server.JsonHelpers.deserialize(JsonHelpers.java:71) ~[na:na]
    ... 11 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
    at java.base@21.0.1/java.lang.reflect.Method.invoke(Method.java:580) ~[sd-backend-template:na]
    at com.launchdarkly.shaded.com.google.gson.internal.UnsafeAllocator$1.newInstance(UnsafeAllocator.java:50) ~[na:na]
    at com.launchdarkly.shaded.com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:225) ~[na:na]
    ... 16 common frames omitted
Caused by: java.lang.IllegalArgumentException: Type com.launchdarkly.sdk.server.JsonHelpers$PostProcessingDeserializableTypeAdapterFactory is instantiated reflectively but was never registered. Register the type by adding "unsafeAllocated" for the type in reflect-config.json.
    at org.graalvm.nativeimage.builder/com.oracle.svm.core.graal.snippets.SubstrateAllocationSnippets.instanceHubErrorStub(SubstrateAllocationSnippets.java:315) ~[na:na]
    at jdk.unsupported@21.0.1/sun.misc.Unsafe.allocateInstance(Unsafe.java:884) ~[sd-backend-template:na]
    ... 19 common frames omitted
tanderson-ld commented 4 months ago

Hello again @ketronkowski. I am working through deeper investigation of this issue. I have reproduced the issue locally and am able to remedy it by providing various -config.json files in the META-INF/native-image folder of the project. The -config.json files I am using I got by using the graal tracing agent with the following argument.

-agentlib:native-image-agent=config-output-dir=META-INF/native-image

Is that what you used with the test suite that you eluded to?

I am investigating further if we can integrate that into our contract testing system as that exercises more unique code paths I believe. I also have to discuss the long term stability concerns of this approach with the team.

tanderson-ld commented 4 months ago

@ketronkowski , I was able to use our contract testing infrastructure to generate the *-config.json files, however inside those files are many classes that LaunchDarkly is not responsible for including third party libraries that we import as well as some that we don't explicitly import. For example: "name":"apple.security.AppleProvider", . I expect this Apple one appears because I was running this on a Mac. It seems we start to get into the problem of which native platform dependencies are needed where and calling those out, which is ultimately what the JVM was intended to solve.

I discussed with a few members of the team regarding the technique of collecting this information, doing so in an automated way, as well as the potential fragilities of this mechanism. We believe it is likely better at this stage of support and community demand to leave it to you and the community to implement it in the graalvm-reachability-repository repository. One caution is that we may restructure our internal classes at some point in the future. We don't have any plans at the moment to do so, but thought it worth calling out that there may be a point in the future when the metadata needs some revision.

Please let us know your thoughts and if you think we have any incorrect understandings around the problem scope or details.

ketronkowski commented 4 months ago

I am fine with that approach with the reachability-repository at this point and would be happy to start the PR process.

I assume that if there are any questions (about why I am making the PR request) I can point the maintainers back to this issue so they can see the background of the issue and why I am the one posting the PR.

tanderson-ld commented 4 months ago

Please do! Let us know when you open the PR and we can help review as well. Thank you for helping resolve this.