Skip to content
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

Limit external configuration sources parsing in native-mode #42140

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

zakkak
Copy link
Contributor

@zakkak zakkak commented Jul 25, 2024

SmallryeConfigBuilder tries to access properties files from various sources, despite them not being available at run-time nor supported in native-mode, see #41994

This patch also avoids getting a MissingRegistrationError when using -H:+ThrowMissingRegistrationErrors or --exact-reachability-metadata.

Related to #41994 and #41995

Supersedes #42103

@zakkak
Copy link
Contributor Author

zakkak commented Jul 25, 2024

@radcortez please take a look. I opened it as draft since there are way more reflective access being caught by -H:ThrowMissingRegistrationErrors=

The ones I am trying to understand now are ones generated because of the sources automatically added to the RunTimeConfigCustomizer. Do you think we could/should skip some of those when targeting native-mode, or are they all necessary?

Since I have your attention I would also like to ask why you are not generating ServiceProviderBuildItems instead of ReflectiveClassBuildItems in

reflectiveClass.produce(ReflectiveClassBuildItem.builder(service).build());

Thanks

@radcortez
Copy link
Member

The ones I am trying to understand now are ones generated because of the sources automatically added to the RunTimeConfigCustomizer. Do you think we could/should skip some of those when targeting native-mode, or are they all necessary?

The idea is to discover all the services during the build and generate a config customizer that programmatically registers all the discovered services. I think we cannot skip any of the things we discover. Are you seeing issues with this?

Since I have your attention I would also like to ask why you are not generating ServiceProviderBuildItems instead of ReflectiveClassBuildItems in

We probably don't need the reflection registration anymore. We can try to drop it and check :)

@@ -391,6 +392,17 @@ public void setupConfigOverride(

@BuildStep
public void watchConfigFiles(BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles) {
Copy link
Member

@radcortez radcortez Jul 25, 2024

Choose a reason for hiding this comment

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

The watched files include external files like .env and {user.dir}/config/application{-profile}.properties. Should these be registered as a resource with the native image?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anything accessed through java.lang.ClassLoader.getResources (invoked by https://github.com/smallrye/smallrye-common/blob/c29bb1c00f484f5fa86f183e75c37af462ac6546/classloader/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java#L84) needs to be registered (at least in theory).

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, these "issues" are popping up when using -H:+ThrowMissingRegistrationErrors (see #41995) which aims to help developers detect unregistered accesses to elements. What I suspect is that there are many places where Quarkus tries to access unregistered elements and just fails without a warning, but since there are other places or ways to get the same data back it works as expected.

So in many of these cases it might be OK to not register the elements, but at the same time the best practice would be to ensure hat Quarkus also doesn't try to access them at run-time (e.g. through substitutions or changing configuration).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@radcortez note that when the uri scheme is not defined smallrye tries both tryFileSystem and tryClassPath in https://github.com/smallrye/smallrye-config/blob/e8d87ce1cfbf1e6f259a798c85498267dd12b32b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java#L103-L104 with the latter accessing (or trying to) the config files through java.lang.ClassLoader.getResources.

If we don't want the external files to even be tried I think we should alter the value of smallrye.config.locations. WDYT?

List<String> configWatchedFiles = new ArrayList<>();
String userDir = System.getProperty("user.dir");

// Main files
configWatchedFiles.add("META-INF/microprofile-config.yaml");
configWatchedFiles.add("META-INF/microprofile-config.yml");
configWatchedFiles.add("application.yaml");
configWatchedFiles.add("application.yml");
configWatchedFiles.add(Paths.get(userDir, "config", "application.yaml").toAbsolutePath().toString());
Copy link
Member

Choose a reason for hiding this comment

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

Check previous comments about external files.

@zakkak
Copy link
Contributor Author

zakkak commented Jul 26, 2024

The idea is to discover all the services during the build and generate a config customizer that programmatically registers all the discovered services. I think we cannot skip any of the things we discover. Are you seeing issues with this?

Not necessarily, it kind of complicates the resource registration process so I am just trying to see if there is any opportunity for trimming down :)

@zakkak
Copy link
Contributor Author

zakkak commented Jul 26, 2024

We probably don't need the reflection registration anymore. We can try to drop it and check :)

It looks like you are right.

@zakkak zakkak force-pushed the 2024-07-24-smallryeconfigsources branch from fc678ed to 628ee63 Compare July 26, 2024 09:54
@radcortez
Copy link
Member

Not necessarily, it kind of complicates the resource registration process so I am just trying to see if there is any opportunity for trimming down :)

Hum, the goal was to avoid as many registrations as possible. Since this has been an ongoing process, there are probably some code leftovers that are not required anymore (the reflection registration of services, for instance).

Let me know what you are seeing and we try to hax a few things.

@radcortez
Copy link
Member

I think we can drop registrations of ServiceProvider in

// XXX replace this with constant-folded service loader impl
@BuildStep
void nativeServiceProviders(
final BuildProducer<ServiceProviderBuildItem> providerProducer) throws IOException {
providerProducer.produce(new ServiceProviderBuildItem(ConfigProviderResolver.class.getName(),
SmallRyeConfigProviderResolver.class.getName()));
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
for (Class<?> serviceClass : Arrays.asList(
Converter.class,
ConfigSourceInterceptor.class,
ConfigSourceInterceptorFactory.class,
SecretKeysHandler.class,
SecretKeysHandlerFactory.class,
ConfigValidator.class)) {
final String serviceName = serviceClass.getName();
final Set<String> names = ServiceUtil.classNamesNamedIn(classLoader, SERVICES_PREFIX + serviceName);
if (!names.isEmpty()) {
providerProducer.produce(new ServiceProviderBuildItem(serviceName, names));
}
}
}

for everything except ConfigValidator which we don't generate yet.

@zakkak
Copy link
Contributor Author

zakkak commented Jul 26, 2024

Let me know what you are seeing and we try to hax a few things.

If you run:

./mvnw -Dnative -pl integration-tests/smallrye-config/ -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean verify -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-22 -Dquarkus.native.additional-build-args=-H:ThrowMissingRegistrationErrors=,-H:MissingRegistrationReportingMode=Warn

you will see a bunch of exceptions being thrown, indicating that the resulting native-image is trying to access elements that have not been registered at build time. Unfortunately the Warn mode limits the trace to just 4 lines, so you can't get enough details from it. FYI I am attaching the output of the above command when using a modified Mandrel build without the 4 lines limit.

exceptions.log

A couple of indicative examples:

com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError: The program tried to access the resource at path 

   META-INF/microprofile-config.yaml

 without it being registered as reachable. Add it to the resource metadata to solve this problem. See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#resources-and-resource-bundles for help
  java.base@22.0.1-beta/java.lang.ClassLoader.getResources(ClassLoader.java:99)
  io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths(ClassPathUtils.java:84)
  io.smallrye.config.AbstractLocationConfigSourceLoader.tryClassPath(AbstractLocationConfigSourceLoader.java:141)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:104)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:87)
  io.smallrye.config.source.yaml.YamlConfigSourceLoader$InClassPath.getConfigSources(YamlConfigSourceLoader.java:37)
  io.smallrye.config.source.yaml.YamlConfigSourceLoader$InClassPath.getConfigSources(YamlConfigSourceLoader.java:31)
  io.smallrye.config.SmallRyeConfig$ConfigSources.buildSources(SmallRyeConfig.java:832)
  io.smallrye.config.SmallRyeConfig$ConfigSources.<init>(SmallRyeConfig.java:770)
  io.smallrye.config.SmallRyeConfig.<init>(SmallRyeConfig.java:85)
  io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:736)
  io.quarkus.runtime.generated.Config.readConfig(Unknown Source)
  io.quarkus.runtime.generated.Config.createRunTimeConfig(Unknown Source)
  io.quarkus.deployment.steps.RuntimeConfigSetup.deploy(Unknown Source)
  io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
  io.quarkus.runtime.Application.start(Application.java:101)
  io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:111)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
  io.quarkus.runner.GeneratedMain.main(Unknown Source)
com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError: The program tried to access the resource at path 
org.graalvm.nativeimage.MissingReflectionRegistrationError: The program tried to reflectively access

   io.quarkus.datasource.runtime.DataSourcesRuntimeConfig.getDeclaredMethods()

 without it being registered for runtime reflection. Add io.quarkus.datasource.runtime.DataSourcesRuntimeConfig.getDeclaredMethods() to the reflection metadata to solve this problem. See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#reflection for help.
  java.base@22.0.1-beta/java.lang.Class.getDeclaredMethods(DynamicHub.java:1165)
  io.smallrye.config.ConfigMappingInterface.createConfigurationInterface(ConfigMappingInterface.java:757)
  io.smallrye.config.ConfigMappingInterface$1.computeValue(ConfigMappingInterface.java:37)
  io.smallrye.config.ConfigMappingInterface$1.computeValue(ConfigMappingInterface.java:35)
  java.base@22.0.1-beta/java.lang.ClassValue.get(JavaLangSubstitutions.java:531)
  io.smallrye.config.ConfigMappingInterface.getConfigurationInterface(ConfigMappingInterface.java:52)
  io.smallrye.config.validator.BeanValidationConfigValidator.validateMapping(BeanValidationConfigValidator.java:38)
  io.smallrye.config.SmallRyeConfig.getConfigMapping(SmallRyeConfig.java:604)
  io.quarkus.deployment.steps.DataSourcesExcludedFromHealthChecksProcessor$produceBean807665441.deploy_0(Unknown Source)
  io.quarkus.deployment.steps.DataSourcesExcludedFromHealthChecksProcessor$produceBean807665441.deploy(Unknown Source)
  io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
  io.quarkus.runtime.Application.start(Application.java:101)
  io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:111)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
  io.quarkus.runner.GeneratedMain.main(Unknown Source)
org.graalvm.nativeimage.MissingReflectionRegistrationError: The program tried to reflectively access

@radcortez
Copy link
Member

If you run:

./mvnw -Dnative -pl integration-tests/smallrye-config/ -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean verify -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-22 -Dquarkus.native.additional-build-args=-H:ThrowMissingRegistrationErrors=,-H:MissingRegistrationReportingMode=Warn

you will see a bunch of exceptions being thrown, indicating that the resulting native-image is trying to access elements that have not been registered at build time. Unfortunately the Warn mode limits the trace to just 4 lines, so you can't get enough details from it. FYI I am attaching the output of the above command when using a modified Mandrel build without the 4 lines limit.

Great. Thanks.

Ok, let's try to trim these down. Did you run with these PR changes? It shouldn't be looking for *.properties files in the classpath.

@zakkak
Copy link
Contributor Author

zakkak commented Jul 29, 2024

Did you run with these PR changes?

Yes, it removes some of these warnings but not all.

@radcortez
Copy link
Member

Yes, it removes some of these warnings but not all.

Yes, some of the warnings related to the lookups to the config files should be gone now.

Looking into other warnings, I see many related to the Validator. The issue here is that we only register for reflection the config classes that have validation annotations. The config code still calls getDeclaredMethods to no effect because there are no properties to validate, so it doesn't really fail.

There is no quick fix for this. I guess the best option is to override the Validator and pass in a list of config classes that require validation to avoid that call.

Copy link

github-actions bot commented Dec 2, 2024

🎊 PR Preview 8bfec22 has been successfully built and deployed to https://quarkus-pr-main-42140-preview.surge.sh/version/main/guides/

  • Images of blog posts older than 3 months are not available.
  • Newsletters older than 3 months are not available.

`SmallryeConfigBuilder` tries to access properties files from various
sources, despite them not being available at run-time nor supported in
native-mode, see quarkusio#41994

This patch also avoids getting a `MissingRegistrationError` when using
`-H:+ThrowMissingRegistrationErrors` or `--exact-reachability-metadata`.

Related to quarkusio#41994 and
quarkusio#41995
@zakkak zakkak force-pushed the 2024-07-24-smallryeconfigsources branch from c005383 to 0f37d9f Compare December 2, 2024 11:04
@zakkak
Copy link
Contributor Author

zakkak commented Dec 2, 2024

@radcortez please take another look.

@zakkak zakkak requested a review from radcortez December 2, 2024 11:14
@zakkak zakkak marked this pull request as ready for review December 2, 2024 11:14

This comment has been minimized.

Copy link

quarkus-bot bot commented Dec 2, 2024

Status for workflow Quarkus Documentation CI

This is the status report for running Quarkus Documentation CI on commit 0f37d9f.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

Warning

There are other workflow runs running, you probably need to wait for their status before merging.

@radcortez
Copy link
Member

@radcortez please take another look.

Ok, what is the goal now? :)

Is it to include the configuration files in the native image because of #44652 (comment)?

Isn't this going to hurt the native image startup time?

Copy link

quarkus-bot bot commented Dec 2, 2024

Status for workflow Quarkus CI

This is the status report for running Quarkus CI on commit 0f37d9f.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the Develocity build scans.


Flaky tests - Develocity

⚙️ JVM Tests - JDK 17

📦 extensions/panache/hibernate-reactive-rest-data-panache/deployment

io.quarkus.hibernate.reactive.rest.data.panache.deployment.repository.PanacheRepositoryResourcePutMethodTest.shouldUpdateComplexObject - History

  • 1 expectation failed. JSON path name doesn't match. Expected: is "updated collection" Actual: empty collection - java.lang.AssertionError
java.lang.AssertionError: 
1 expectation failed.
JSON path name doesn't match.
Expected: is "updated collection"
  Actual: empty collection

	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)

@zakkak
Copy link
Contributor Author

zakkak commented Dec 2, 2024

Ok, what is the goal now? :)

The goal is to not have code accessing unregistered resources. The trivial approach is to just register everything that's being accessed, but as you say this might impact performance. So ideally we would like to eliminate unnecessary accesses (and as a result avoid the registration of the corresponding resources).

The first step towards that was to use a different SmallRyeConfigBuilder for native builds, but this still requires some registrations, as seen in the current state of the PR. To my understanding we can further reduce these registrations by "altering" some build time configurations, e.g. smallrye.config.locations, but I am not sure about the implications or what would be the best way to achieve it.

The next step I guess is what you describe in #44652 (review)

Actually, we already generate a class with the configuration from all the resources found in the classpath, so we don't need to include additional resources or even load them.

I've been wanting to disable this for a while, but I have yet to determine the implications in JVM mode, so I guess the right fix for now it is to disable it for native image only.

but that's something I can't handle myself :)

@radcortez
Copy link
Member

The next step I guess is what you describe in #44652 (review)

Yes, that is why I was asking. Because for native image we don't use the resources, so just disabling some of the discovery pieces would be enough. That problem would still exist, but it is no different from what we have today.

Everything that comes from the classpath is recorded directly in bytecode. It is bit tricky to follow it in the code.

Here, we collect the values:

Set<String> allProperties = getAllProperties(registeredRoots);
Set<String> unknownBuildProperties = new HashSet<>();
for (String propertyName : allProperties) {
if (propertyName.equals(ConfigSource.CONFIG_ORDINAL)) {
continue;
}
if (deprecatedProperties.contains(propertyName)) {
log.warnf("The '%s' config property is deprecated and should not be used anymore", propertyName);
}
NameIterator ni = new NameIterator(propertyName);
if (ni.hasNext() && PropertiesUtil.isPropertyInRoots(propertyName, registeredRoots)) {
// build time patterns
Container matched = buildTimePatternMap.match(ni);
boolean knownProperty = matched != null;
if (matched instanceof FieldContainer) {
ConfigValue configValue = config.getConfigValue(propertyName);
if (configValue.getValue() != null) {
allBuildTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
ni.goToEnd();
// cursor is located after group property key (if any)
getGroup((FieldContainer) matched, ni);
// we don't have to set the field because the group object init does it for us
}
} else if (matched != null) {
assert matched instanceof MapContainer;
ConfigValue configValue = config.getConfigValue(propertyName);
if (configValue.getValue() != null) {// it's a leaf value within a map
// these must always be explicitly set
ni.goToEnd();
// cursor is located after the map key
String key = ni.getPreviousSegment();
Map<String, Object> map = getMap((MapContainer) matched, ni);
// we always have to set the map entry ourselves
Field field = matched.findField();
Converter<?> converter = getConverter(config, field, ConverterType.of(field));
map.put(key, config.convertValue(configValue, converter));
allBuildTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
}
}
// build time (run time visible) patterns
ni.goToStart();
matched = buildTimeRunTimePatternMap.match(ni);
knownProperty = knownProperty || matched != null;
if (matched instanceof FieldContainer) {
ConfigValue configValue = config.getConfigValue(propertyName);
if (configValue.getValue() != null) {
ni.goToEnd();
// cursor is located after group property key (if any)
getGroup((FieldContainer) matched, ni);
allBuildTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
buildTimeRunTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
}
} else if (matched != null) {
assert matched instanceof MapContainer;
ConfigValue configValue = config.getConfigValue(propertyName);
if (configValue.getValue() != null) {// it's a leaf value within a map
// these must always be explicitly set
ni.goToEnd();
// cursor is located after the map key
String key = ni.getPreviousSegment();
Map<String, Object> map = getMap((MapContainer) matched, ni);
// we always have to set the map entry ourselves
Field field = matched.findField();
Converter<?> converter = getConverter(config, field, ConverterType.of(field));
map.put(key, config.convertValue(configValue, converter));
// cache the resolved value
allBuildTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
buildTimeRunTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
}
}
// run time patterns
ni.goToStart();
matched = runTimePatternMap.match(ni);
knownProperty = knownProperty || matched != null;
if (matched != null) {
// it's a run-time default (record for later)
ConfigValue configValue = withoutExpansion(() -> runtimeConfig.getConfigValue(propertyName));
if (configValue.getValue() != null) {
runTimeValues.put(configValue.getNameProfiled(), configValue.getValue());
}
}
if (!knownProperty) {
unknownBuildProperties.add(propertyName);
}
} else {
// it's not managed by us; record it
ConfigValue configValue = withoutExpansion(() -> runtimeConfig.getConfigValue(propertyName));
if (configValue.getValue() != null) {
runTimeValues.put(propertyName, configValue.getValue());
}
// in the case the user defined compound keys in YAML (or similar config source, that quotes the name)
if (PropertiesUtil.isPropertyQuarkusCompoundName(ni)) {
unknownBuildProperties.add(propertyName);
}
}
}
// Remove properties coming from the Build System, because most likely they are used in build scripts
config.getConfigSource("PropertiesConfigSource[source=Build system]").ifPresent(
configSource -> unknownBuildProperties.removeAll(configSource.getPropertyNames()));
// ConfigMappings
for (ConfigClass mapping : buildTimeVisibleMappings) {
objectsByClass.put(mapping.getKlass(), config.getConfigMapping(mapping.getKlass(), mapping.getPrefix()));
}
Set<PropertyName> buildTimeNames = mappingsToNames(buildTimeMappings).keySet();
Set<PropertyName> buildTimeRunTimeNames = mappingsToNames(buildTimeRunTimeMappings).keySet();
Set<PropertyName> runTimeNames = mappingsToNames(runTimeMappings).keySet();
for (String property : allProperties) {
PropertyName name = new PropertyName(property);
if (buildTimeNames.contains(name)) {
unknownBuildProperties.remove(property);
ConfigValue value = config.getConfigValue(property);
if (value.getRawValue() != null) {
allBuildTimeValues.put(value.getNameProfiled(), value.getRawValue());
}
}
if (buildTimeRunTimeNames.contains(name)) {
unknownBuildProperties.remove(property);
ConfigValue value = config.getConfigValue(property);
if (value.getRawValue() != null) {
allBuildTimeValues.put(value.getNameProfiled(), value.getRawValue());
buildTimeRunTimeValues.put(value.getNameProfiled(), value.getRawValue());
}
}
if (runTimeNames.contains(name)) {
unknownBuildProperties.remove(property);
ConfigValue value = runtimeConfig.getConfigValue(property);
if (value.getRawValue() != null) {
runTimeValues.put(value.getNameProfiled(), value.getRawValue());
}
}
}

We use a customized instance of SmallRyeConfig:

/**
* Use this Config instance to record the runtime default values. We cannot use the main Config
* instance because it may record values coming from local development sources (Environment Variables,
* System Properties, etc.) in at build time. Local config source values may be completely different between the
* build environment and the runtime environment, so it doesn't make sense to record these.
*
* @return a new {@link SmallRyeConfig} instance without the local sources, including SysPropConfigSource,
* EnvConfigSource, .env, and Build system sources.
*/
private SmallRyeConfig getConfigForRuntimeRecording() {
SmallRyeConfigBuilder builder = ConfigUtils.emptyConfigBuilder();
// Do not use a profile, so we can record both profile properties and main properties of the active profile
builder.getProfiles().add("");
builder.getSources().clear();
builder.getSourceProviders().clear();
builder.withCustomizers(new SmallRyeConfigBuilderCustomizer() {
@Override
public void configBuilder(final SmallRyeConfigBuilder builder) {
builder.getMappingsBuilder().getMappings().clear();
}
@Override
public int priority() {
return Integer.MAX_VALUE;
}
});
builder.setAddDefaultSources(false)
// Customizers may duplicate sources, but not much we can do about it, we need to run them
.addDiscoveredCustomizers()
.addPropertiesSources();
// TODO - Should we reset quarkus.config.location to not record from these sources?
for (ConfigSource configSource : config.getConfigSources()) {
if (configSource instanceof SysPropConfigSource) {
continue;
}
if (configSource instanceof EnvConfigSource) {
continue;
}
if ("PropertiesConfigSource[source=Build system]".equals(configSource.getName())) {
continue;
}
builder.withSources(configSource);
}
builder.withSources(new AbstractConfigSource("Profiles", Integer.MAX_VALUE) {
private final Set<String> profiles = Set.of(
"quarkus.profile",
"quarkus.config.profile.parent",
"quarkus.test.profile",
SMALLRYE_CONFIG_PROFILE,
SMALLRYE_CONFIG_PROFILE_PARENT,
Config.PROFILE);
@Override
public Set<String> getPropertyNames() {
return Collections.emptySet();
}
@Override
public String getValue(final String propertyName) {
if (profiles.contains(propertyName)) {
return config.getConfigValue(propertyName).getValue();
}
return null;
}
});
return builder.build();
}

And we write the values here:

@BuildStep
void buildTimeRunTimeConfig(
ConfigurationBuildItem configItem,
BuildProducer<GeneratedClassBuildItem> generatedClass,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<StaticInitConfigBuilderBuildItem> staticInitConfigBuilder,
BuildProducer<RunTimeConfigBuilderBuildItem> runTimeConfigBuilder) {
String builderClassName = "io.quarkus.runtime.generated.BuildTimeRunTimeFixedConfigSourceBuilder";
try (ClassCreator classCreator = ClassCreator.builder()
.classOutput(new GeneratedClassGizmoAdaptor(generatedClass, true))
.className(builderClassName)
.interfaces(ConfigBuilder.class)
.setFinal(true)
.build()) {
FieldDescriptor source = FieldDescriptor.of(classCreator.getClassName(), "source", ConfigSource.class);
classCreator.getFieldCreator(source).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL);
MethodCreator clinit = classCreator.getMethodCreator("<clinit>", void.class);
clinit.setModifiers(Opcodes.ACC_STATIC);
ResultHandle map = clinit.newInstance(MethodDescriptor.ofConstructor(HashMap.class));
MethodDescriptor put = MethodDescriptor.ofMethod(Map.class, "put", Object.class, Object.class, Object.class);
for (Map.Entry<String, String> entry : configItem.getReadResult().getBuildTimeRunTimeValues().entrySet()) {
clinit.invokeInterfaceMethod(put, map, clinit.load(entry.getKey()), clinit.load(entry.getValue()));
}
ResultHandle defaultValuesSource = clinit.newInstance(
MethodDescriptor.ofConstructor(DefaultValuesConfigSource.class, Map.class, String.class, int.class), map,
clinit.load("BuildTime RunTime Fixed"), clinit.load(Integer.MAX_VALUE));
ResultHandle disableableConfigSource = clinit.newInstance(
MethodDescriptor.ofConstructor(DisableableConfigSource.class, ConfigSource.class),
defaultValuesSource);
clinit.writeStaticField(source, disableableConfigSource);
clinit.returnVoid();
MethodCreator method = classCreator.getMethodCreator(CONFIG_BUILDER);
ResultHandle configBuilder = method.getMethodParam(0);
ResultHandle configSources = method.newArray(ConfigSource.class, 1);
method.writeArrayValue(configSources, 0, method.readStaticField(source));
method.invokeVirtualMethod(WITH_SOURCES, configBuilder, configSources);
method.returnValue(configBuilder);
}
reflectiveClass.produce(ReflectiveClassBuildItem.builder(builderClassName).reason(getClass().getName()).build());
staticInitConfigBuilder.produce(new StaticInitConfigBuilderBuildItem(builderClassName));
runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(builderClassName));
}

Map<String, String> defaultValues = new HashMap<>();
// Default values from @ConfigRoot
defaultValues.putAll(configItem.getReadResult().getRunTimeDefaultValues());
// Default values from build item RunTimeConfigurationDefaultBuildItem override
for (RunTimeConfigurationDefaultBuildItem e : runTimeDefaults) {
defaultValues.put(e.getKey(), e.getValue());
}
// Recorded values from build time from any other source (higher ordinal then defaults, so override)
List<String> profiles = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).getProfiles();
for (Map.Entry<String, String> entry : configItem.getReadResult().getRunTimeValues().entrySet()) {
// Runtime values may contain active profiled names that override sames names in defaults
// We need to keep the original name definition in case a different profile is used to run the app
String activeName = ProfileConfigSourceInterceptor.activeName(entry.getKey(), profiles);
// But keep the default
if (!configItem.getReadResult().getRunTimeDefaultValues().containsKey(activeName)) {
defaultValues.remove(activeName);
}
defaultValues.put(entry.getKey(), entry.getValue());
}
defaultValues.putAll(configItem.getReadResult().getRunTimeValues());

Let me have a look and try to come up with something that can avoid these lookups, but without requiring the resouces.

@radcortez
Copy link
Member

Here is an alternate proposal: #44910

Let's see if nothing breaks.

@zakkak
Copy link
Contributor Author

zakkak commented Dec 5, 2024

Here is an alternate proposal: #44910

I gave #44910 a try and I still see the following accesses:

com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError: The program tried to access the resource at path

   config.properties

without it being registered as reachable. Add it to the resource metadata to solve this problem. See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#resources-and-resource-bundles for help
  null
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.resources.MissingResourceRegistrationUtils.missingResource(MissingResourceRegistrationUtils.java:44)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.Resources.createURLs(Resources.java:494)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.Resources.createURLs(Resources.java:462)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.ResourcesHelper.nameToResourceListURLs(ResourcesHelper.java:57)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.ResourcesHelper.nameToResourceEnumerationURLs(ResourcesHelper.java:66)
  java.base@24/java.lang.ClassLoader.getResources(ClassLoader.java:93)
  io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths(ClassPathUtils.java:84)
  io.smallrye.config.AbstractLocationConfigSourceLoader.tryClassPath(AbstractLocationConfigSourceLoader.java:144)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:107)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:94)
  io.smallrye.config.AbstractLocationConfigSourceFactory.getConfigSources(AbstractLocationConfigSourceFactory.java:32)
  io.smallrye.config.ConfigurableConfigSource.getConfigSources(ConfigurableConfigSource.java:50)
  io.smallrye.config.SmallRyeConfig$ConfigSources.mapLateSources(SmallRyeConfig.java:974)
  io.smallrye.config.SmallRyeConfig$ConfigSources.<init>(SmallRyeConfig.java:841)
  io.smallrye.config.SmallRyeConfig.<init>(SmallRyeConfig.java:86)
  io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:767)
  io.quarkus.runtime.generated.Config.readConfig(Unknown Source)
  io.quarkus.runtime.generated.Config.createRunTimeConfig(Unknown Source)
  io.quarkus.deployment.steps.RuntimeConfigSetup.deploy(Unknown Source)
  io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
  io.quarkus.runtime.Application.start(Application.java:101)
  io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:121)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
  io.quarkus.runner.GeneratedMain.main(Unknown Source)

com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError: The program tried to access the resource at path

   keystore

without it being registered as reachable. Add it to the resource metadata to solve this problem. See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#resources-and-resource-bundles for help
  null
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.resources.MissingResourceRegistrationUtils.missingResource(MissingResourceRegistrationUtils.java:44)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.Resources.createURLs(Resources.java:494)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.Resources.createURLs(Resources.java:462)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.ResourcesHelper.nameToResourceListURLs(ResourcesHelper.java:57)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.ResourcesHelper.nameToResourceEnumerationURLs(ResourcesHelper.java:66)
  java.base@24/java.lang.ClassLoader.getResources(ClassLoader.java:93)
  io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths(ClassPathUtils.java:84)
  io.smallrye.config.AbstractLocationConfigSourceLoader.tryClassPath(AbstractLocationConfigSourceLoader.java:144)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:107)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:94)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:86)
  io.smallrye.config.source.keystore.KeyStoreConfigSourceFactory$2.getConfigSources(KeyStoreConfigSourceFactory.java:110)
  io.smallrye.config.source.keystore.KeyStoreConfigSourceFactory.loadKeyStoreSources(KeyStoreConfigSourceFactory.java:113)
  io.smallrye.config.source.keystore.KeyStoreConfigSourceFactory.getConfigSources(KeyStoreConfigSourceFactory.java:76)
  io.smallrye.config.ConfigurableConfigSource.getConfigSources(ConfigurableConfigSource.java:50)
  io.smallrye.config.SmallRyeConfig$ConfigSources.mapLateSources(SmallRyeConfig.java:974)
  io.smallrye.config.SmallRyeConfig$ConfigSources.<init>(SmallRyeConfig.java:841)
  io.smallrye.config.SmallRyeConfig.<init>(SmallRyeConfig.java:86)
  io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:767)
  io.quarkus.runtime.generated.Config.readConfig(Unknown Source)
  io.quarkus.runtime.generated.Config.createRunTimeConfig(Unknown Source)
  io.quarkus.deployment.steps.RuntimeConfigSetup.deploy(Unknown Source)
  io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
  io.quarkus.runtime.Application.start(Application.java:101)
  io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:121)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
  io.quarkus.runner.GeneratedMain.main(Unknown Source)

com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError: The program tried to access the resource at path

   config.properties

without it being registered as reachable. Add it to the resource metadata to solve this problem. See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#resources-and-resource-bundles for help
  null
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.resources.MissingResourceRegistrationUtils.missingResource(MissingResourceRegistrationUtils.java:44)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.Resources.createURLs(Resources.java:494)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.Resources.createURLs(Resources.java:462)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.ResourcesHelper.nameToResourceListURLs(ResourcesHelper.java:57)
  org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.ResourcesHelper.nameToResourceEnumerationURLs(ResourcesHelper.java:66)
  java.base@24/java.lang.ClassLoader.getResources(ClassLoader.java:93)
  io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths(ClassPathUtils.java:84)
  io.smallrye.config.AbstractLocationConfigSourceLoader.tryClassPath(AbstractLocationConfigSourceLoader.java:144)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:107)
  io.smallrye.config.AbstractLocationConfigSourceLoader.loadConfigSources(AbstractLocationConfigSourceLoader.java:94)
  io.smallrye.config.AbstractLocationConfigSourceFactory.getConfigSources(AbstractLocationConfigSourceFactory.java:32)
  io.smallrye.config.PropertiesLocationConfigSourceFactory.getConfigSources(PropertiesLocationConfigSourceFactory.java:16)
  io.smallrye.config.ConfigurableConfigSource.getConfigSources(ConfigurableConfigSource.java:50)
  io.smallrye.config.SmallRyeConfig$ConfigSources.mapLateSources(SmallRyeConfig.java:974)
  io.smallrye.config.SmallRyeConfig$ConfigSources.<init>(SmallRyeConfig.java:841)
  io.smallrye.config.SmallRyeConfig.<init>(SmallRyeConfig.java:86)
  io.smallrye.config.SmallRyeConfigBuilder.build(SmallRyeConfigBuilder.java:767)
  io.quarkus.runtime.generated.Config.readConfig(Unknown Source)
  io.quarkus.runtime.generated.Config.createRunTimeConfig(Unknown Source)
  io.quarkus.deployment.steps.RuntimeConfigSetup.deploy(Unknown Source)
  io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
  io.quarkus.runtime.Application.start(Application.java:101)
  io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:121)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
  io.quarkus.runtime.Quarkus.run(Quarkus.java:124)
  io.quarkus.runner.GeneratedMain.main(Unknown Source)

@radcortez
Copy link
Member

Which example are you using?

One general problem is how do we handle libraries that may look for stuff but not necessarily require a Quarkus extension? For instance, I notice there that the app uses smallrye-config-source-keystore, which doesn't really require any special handling, so users just add that as a dependency directly.

It does seem silly to require an extension to remove / register resources for every dependency, and impractical to have to do this for every possible combination. Also, I think core shouldn't have to know which resources a dependency might need, because we will be leaking dependencies requirements into the core that may or may not be in the project.

What should we do in this cases?

@zakkak
Copy link
Contributor Author

zakkak commented Dec 5, 2024

Which example are you using?

./mvnw -Dnative -pl integration-tests/smallrye-config/ -Dnative.surefire.skip -Dformat.skip -Dno-descriptor-tests clean package -Dquarkus.native.container-runtime=podman -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-23 -Dquarkus.native.additional-build-args=-H:ThrowMissingRegistrationErrors= 
./integration-tests/smallrye-config/target/quarkus-integration-test-smallrye-config-999-SNAPSHOT-runner -XX:MissingRegistrationReportingMode=Warn

One general problem is how do we handle libraries that may look for stuff but not necessarily require a Quarkus extension? For instance, I notice there that the app uses smallrye-config-source-keystore, which doesn't really require any special handling, so users just add that as a dependency directly.

It does seem silly to require an extension to remove / register resources for every dependency, and impractical to have to do this for every possible combination. Also, I think core shouldn't have to know which resources a dependency might need, because we will be leaking dependencies requirements into the core that may or may not be in the project.

What should we do in this cases?

I guess we should rely on the library doing the necessary registrations or on https://github.com/oracle/graalvm-reachability-metadata

@radcortez
Copy link
Member

I guess we should rely on the library doing the necessary registrations or on https://github.com/oracle/graalvm-reachability-metadata

Wouldn't that only work when we have a resource? In this case (as in most configuration), the resources are optional. Would you still get an error if you try to register an optional resource (and look it up)?

@zakkak
Copy link
Contributor Author

zakkak commented Dec 5, 2024

Wouldn't that only work when we have a resource?

The registration works even if the resource is not present. It just marks the resource as "accessible", and if it's not there at run-time the program is supposed to behave as it would in JVM-mode. Note that this is going to affect all apps, not just Quarkus apps, so it's likely there will be some change that will relax the rules, but haven't seen anything like that happening yet. I will bring it up again upstream.

Update: Upstream discussion oracle/graal#10264

In this case (as in most configuration), the resources are optional. Would you still get an error if you try to register an optional resource (and look it up)?

No if you register an optional resource that is missing there will be no errors, neither at build time nor at run time.

@radcortez
Copy link
Member

Ok, thanks for the clarification. Let me see what I can do on the SR Config side.

@radcortez
Copy link
Member

So, I've completely disabled the classpath lookup for native mode.

The problem is that when a user sets a config resource, we look it up in the classpath and in the filesystem, but we don't know where that will be. Because you can set these configurations at runtime (where the resource is not known), there is no way to register it properly, even if it is optional.

I think it is still worth pursuing a way to exclude some calls from the exception.

@zakkak
Copy link
Contributor Author

zakkak commented Dec 12, 2024

So, I've completely disabled the classpath lookup for native mode.

I don't see any updates on this branch, did you forget to push?

The problem is that when a user sets a config resource, we look it up in the classpath and in the filesystem, but we don't know where that will be. Because you can set these configurations at runtime (where the resource is not known), there is no way to register it properly, even if it is optional.

True, that's a limitation of the native compilation, you can't dynamically insert things in the classpath. So we could require any runtime set configuration to come from the filesystem.

A problematic scenario with this would be the following: Assuming I have two configurations prod and dev, I should in theory be able to register both at build-time and get them in the native image's classpath. With the above proposal, however, I would still not be able to access them at run-time as classpath lookup will be disabled, right?

I think it is still worth pursuing a way to exclude some calls from the exception.

Feel free to configure some examples in oracle/graal#10264 to strengthen the motivation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants