Skip to content

Commit

Permalink
Provide GraalVM metadata and substitutions (#1357)
Browse files Browse the repository at this point in the history
  • Loading branch information
stIncMale authored Sep 23, 2024
1 parent 7e3108b commit eea937c
Show file tree
Hide file tree
Showing 20 changed files with 357 additions and 529 deletions.
17 changes: 17 additions & 0 deletions bson/src/main/resources/META-INF/native-image/reflect-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"name":"java.lang.Object",
"queryAllDeclaredMethods":true
},
{
"name":"sun.security.provider.NativePRNG",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.SHA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.slf4j.Logger"
}
]
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ ext {
projectReactorVersion = '2022.0.0'
junitBomVersion = '5.10.2'
logbackVersion = '1.3.14'
graalSdkVersion = '24.0.0'
gitVersion = getGitVersion()
}

Expand Down
1 change: 1 addition & 0 deletions driver-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
api "io.netty:netty-buffer", optional
api "io.netty:netty-transport", optional
api "io.netty:netty-handler", optional
compileOnly "org.graalvm.sdk:graal-sdk:$graalSdkVersion"

// Optionally depend on both AWS SDK v2 and v1. The driver will use v2 is present, v1 if present, or built-in functionality if
// neither are present
Expand Down
10 changes: 10 additions & 0 deletions driver-core/src/main/com/mongodb/UnixServerAddress.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
package com.mongodb;

import com.mongodb.annotations.Immutable;
import com.mongodb.internal.graalvm.substitution.UnixServerAddressSubstitution;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;

/**
* Represents the location of a MongoD unix domain socket.
* It is {@linkplain UnixServerAddressSubstitution not supported in GraalVM native image}.
*
* <p>Requires the 'jnr.unixsocket' library.</p>
* @since 3.7
Expand All @@ -34,10 +36,18 @@ public final class UnixServerAddress extends ServerAddress {
/**
* Creates a new instance
* @param path the path of the MongoD unix domain socket.
* @throws UnsupportedOperationException If {@linkplain UnixServerAddressSubstitution called in a GraalVM native image}.
*/
public UnixServerAddress(final String path) {
super(notNull("The path cannot be null", path));
isTrueArgument("The path must end in .sock", path.endsWith(".sock"));
checkNotInGraalVmNativeImage();
}

/**
* @throws UnsupportedOperationException If {@linkplain UnixServerAddressSubstitution called in a GraalVM native image}.
*/
private static void checkNotInGraalVmNativeImage() {
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
import java.util.Hashtable;
import java.util.List;

final class JndiDnsClient implements DnsClient {
/**
* <p>This class is not part of the public API and may be removed or changed at any time</p>
*/
public final class JndiDnsClient implements DnsClient {

@Override
public List<String> getResourceRecordData(final String name, final String type) throws DnsException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb.internal.graalvm.substitution;

import com.mongodb.UnixServerAddress;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

@TargetClass(UnixServerAddress.class)
public final class UnixServerAddressSubstitution {
@Substitute
private static void checkNotInGraalVmNativeImage() {
throw new UnsupportedOperationException("UnixServerAddress is not supported in GraalVM native image");
}

private UnixServerAddressSubstitution() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ Args =\
com.mongodb.UnixServerAddress,\
com.mongodb.internal.connection.SnappyCompressor,\
com.mongodb.internal.connection.ClientMetadataHelper,\
com.mongodb.internal.connection.ServerAddressHelper
com.mongodb.internal.connection.ServerAddressHelper,\
com.mongodb.internal.dns.DefaultDnsResolver
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
[
{
"name":"com.mongodb.BasicDBObject",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.mongodb.MongoNamespace",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"com.mongodb.WriteConcern",
"allPublicFields":true
},
{
"name":"com.mongodb.client.model.changestream.ChangeStreamDocument",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","org.bson.BsonDocument","org.bson.BsonDocument","org.bson.BsonDocument","java.lang.Object","java.lang.Object","org.bson.BsonDocument","org.bson.BsonTimestamp","com.mongodb.client.model.changestream.UpdateDescription","org.bson.BsonInt64","org.bson.BsonDocument","org.bson.BsonDateTime","com.mongodb.client.model.changestream.SplitEvent","org.bson.BsonDocument"] }]
},
{
"name":"com.mongodb.client.model.changestream.SplitEvent",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"com.mongodb.client.model.changestream.TruncatedArray",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"com.mongodb.client.model.changestream.UpdateDescription",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.util.List","org.bson.BsonDocument","java.util.List","org.bson.BsonDocument"] }]
},
{
"name":"java.lang.Record"
},
{
"name":"java.lang.Thread",
"fields":[{"name":"threadLocalRandomProbe"}]
},
{
"name":"java.net.Socket",
"methods":[{"name":"setOption","parameterTypes":["java.net.SocketOption","java.lang.Object"] }]
},
{
"name":"java.security.SecureRandomParameters"
},
{
"name":"java.util.concurrent.ForkJoinTask",
"fields":[{"name":"aux"}, {"name":"status"}]
},
{
"name":"java.util.concurrent.atomic.Striped64",
"fields":[{"name":"base"}, {"name":"cellsBusy"}]
},
{
"name":"jdk.internal.misc.Unsafe"
},
{
"name":"jdk.net.ExtendedSocketOptions",
"fields":[{"name":"TCP_KEEPCOUNT"}, {"name":"TCP_KEEPIDLE"}, {"name":"TCP_KEEPINTERVAL"}]
},
{
"name":"org.bson.codecs.kotlin.DataClassCodecProvider"
},
{
"name":"org.bson.codecs.kotlinx.KotlinSerializerCodecProvider"
},
{
"name":"org.bson.codecs.record.RecordCodecProvider"
},
{
"name":"org.slf4j.Logger"
}
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/services/com.mongodb.spi.dns.DnsClientProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/com.mongodb.spi.dns.InetAddressResolverProvider\\E"
}]},
"bundles":[]
Expand Down
27 changes: 25 additions & 2 deletions graalvm-native-image-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,31 @@ graalvmNative {
// The same is true about executing the `metadataCopy` Gradle task.
// This may be a manifestation of an issue with the `org.graalvm.buildtools.native` plugin.
enabled = false
defaultMode = 'standard'
defaultMode = 'direct'
def taskExecutedWithAgentAttached = 'run'
modes {
direct {
// see https://www.graalvm.org/latest/reference-manual/native-image/metadata/ExperimentalAgentOptions
options.add("config-output-dir=$buildDir/native/agent-output/$taskExecutedWithAgentAttached")
// `experimental-configuration-with-origins` produces
// `graalvm-native-image-app/build/native/agent-output/run/reflect-origins.txt`
// and similar files that explain the origin of each of the reachability metadata piece.
// However, for some reason, the actual reachability metadata is not generated when this option is enabled,
// so enable it manually if you need an explanation for a specific reachability metadata entry,
// and expect the build to fail.
// options.add('experimental-configuration-with-origins')

// `experimental-class-define-support` does not seem to do what it is supposed to do.
// We need this option to work if we want to support `UnixServerAddress` in native image.
// Unfortunately, the tracing agent neither generates the metadata in
// `graalvm-native-image-app/src/main/resources/META-INF/native-image/proxy-config.json`,
// nor does it extract the bytecode of the generated classes to
// `graalvm-native-image-app/src/main/resources/META-INF/native-image/agent-extracted-predefined-classes`.
options.add('experimental-class-define-support')
}
}
metadataCopy {
inputTaskNames.add('run')
inputTaskNames.add(taskExecutedWithAgentAttached)
outputDirectories.add('src/main/resources/META-INF/native-image')
mergeWithExisting = false
}
Expand Down Expand Up @@ -93,4 +115,5 @@ dependencies {
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation platform("io.projectreactor:reactor-bom:$projectReactorVersion")
implementation 'io.projectreactor:reactor-core'
implementation "org.graalvm.sdk:nativeimage:$graalSdkVersion"
}
12 changes: 6 additions & 6 deletions graalvm-native-image-app/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ you need to inform Gradle about that location as specified in https://docs.gradl
Assuming that your MongoDB deployment is accessible at `mongodb://localhost:27017`,
run from the driver project root directory:

| &#x23; | Command | Description |
|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| 0 | `env JAVA_HOME="${JDK17}" ./gradlew -PjavaVersion=21 :graalvm-native-image-app:nativeCompile` | Build the application relying on the reachability metadata stored in `graalvm-native-image-app/src/main/resources/META-INF/native-image`. |
| 1 | `env JAVA_HOME="${JDK17}" ./gradlew :graalvm-native-image-app:clean && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew -PjavaVersion=21 -Pagent :graalvm-native-image-app:run && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew :graalvm-native-image-app:metadataCopy` | Collect the reachability metadata and update the files storing it. Do this before building the application only if building fails otherwise. |
| 2 | `./graalvm-native-image-app/build/native/nativeCompile/NativeImageApp` | Run the application that has been built. |
| 3 | `env JAVA_HOME="${JDK17}" ./gradlew -PjavaVersion=21 :graalvm-native-image-app:nativeRun` | Run the application using Gradle, build it if necessary relying on the stored reachability metadata. |
| &#x23; | Command | Description |
|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| 0 | `env JAVA_HOME="${JDK17}" ./gradlew -PjavaVersion=21 :graalvm-native-image-app:nativeCompile` | Build the application relying on the reachability metadata stored in `graalvm-native-image-app/src/main/resources/META-INF/native-image`. |
| 1 | `env JAVA_HOME="${JDK17}" ./gradlew clean && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew -PjavaVersion=21 -Pagent :graalvm-native-image-app:run && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew :graalvm-native-image-app:metadataCopy` | Collect the reachability metadata and update the files storing it. Do this before building the application only if building fails otherwise. |
| 2 | `./graalvm-native-image-app/build/native/nativeCompile/NativeImageApp` | Run the application that has been built. |
| 3 | `env JAVA_HOME="${JDK17}" ./gradlew -PjavaVersion=21 :graalvm-native-image-app:nativeRun` | Run the application using Gradle, build it if necessary relying on the stored reachability metadata. |

#### Specifying a custom connection string

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb.internal.graalvm;

import com.mongodb.internal.dns.JndiDnsClient;
import com.mongodb.spi.dns.DnsClient;
import com.mongodb.spi.dns.DnsClientProvider;
import com.mongodb.spi.dns.DnsException;

import java.util.List;

import static java.lang.String.format;

public final class CustomDnsClientProvider implements DnsClientProvider {
private static volatile boolean used = false;

public CustomDnsClientProvider() {
}

@Override
public DnsClient create() {
return new CustomDnsClient();
}

static void assertUsed() throws AssertionError {
if (!used) {
throw new AssertionError(format("%s is not used", CustomDnsClientProvider.class.getSimpleName()));
}
}

private static void markUsed() {
used = true;
}

private static final class CustomDnsClient implements DnsClient {
private final JndiDnsClient wrapped;

CustomDnsClient() {
wrapped = new JndiDnsClient();
}

@Override
public List<String> getResourceRecordData(final String name, final String type) throws DnsException {
markUsed();
return wrapped.getResourceRecordData(name, type);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,52 @@
*/
package com.mongodb.internal.graalvm;

import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

final class DnsSpi {
private static final Logger LOGGER = LoggerFactory.getLogger(DnsSpi.class);

public static void main(final String... args) {
LOGGER.info("Begin");
useInetAddressResolverProvider(args);
useDnsClientProvider();
}

private static void useInetAddressResolverProvider(final String... args) {
try (MongoClient client = args.length == 0 ? MongoClients.create() : MongoClients.create(args[0])) {
LOGGER.info("Database names: {}", client.listDatabaseNames().into(new ArrayList<>()));
ArrayList<String> databaseNames = client.listDatabaseNames().into(new ArrayList<>());
LOGGER.info("Database names: {}", databaseNames);
}
CustomInetAddressResolverProvider.assertUsed();
LOGGER.info("End");
}

private static void useDnsClientProvider() {
try (MongoClient client = MongoClients.create(MongoClientSettings.builder()
.applyToClusterSettings(builder -> builder
.srvHost("a.b.c")
// `MongoClient` uses `CustomDnsClientProvider` asynchronously,
// and by waiting for server selection that cannot succeed due to `a.b.c` not resolving to an IP address,
// we give `MongoClient` enough time to use `CustomDnsClientProvider`.
// This is a tolerable race condition for a test.
.serverSelectionTimeout(2, TimeUnit.SECONDS))
.build())) {
ArrayList<String> databaseNames = client.listDatabaseNames().into(new ArrayList<>());
LOGGER.info("Database names: {}", databaseNames);
} catch (RuntimeException e) {
try {
CustomDnsClientProvider.assertUsed();
} catch (AssertionError err) {
err.addSuppressed(e);
throw err;
}
// an exception is expected because `a.b.c` does not resolve to an IP address
}
}

private DnsSpi() {
Expand Down
Loading

0 comments on commit eea937c

Please sign in to comment.