Skip to content

Commit

Permalink
Config: detect injected config value mismatch during static init
Browse files Browse the repository at this point in the history
- record the values injected during static intialization phase
- if the runtime value differs from the injected value the app startup
fails
- also introduce ExecutionMode to easily detect the STATIC_INIT
bootstrap phase
  • Loading branch information
mkouba committed Oct 4, 2023
1 parent 7c26158 commit 46febda
Show file tree
Hide file tree
Showing 17 changed files with 509 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import io.quarkus.gizmo.TryBlock;
import io.quarkus.runtime.Application;
import io.quarkus.runtime.ApplicationLifecycleManager;
import io.quarkus.runtime.ExecutionModeManager;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.NativeImageRuntimePropertiesRecorder;
import io.quarkus.runtime.PreventFurtherStepsException;
Expand Down Expand Up @@ -106,6 +107,14 @@ public class MainClassBuildStep {
void.class, StartupContext.class);
public static final MethodDescriptor CONFIGURE_STEP_TIME_ENABLED = ofMethod(StepTiming.class.getName(), "configureEnabled",
void.class);
public static final MethodDescriptor RUNTIME_EXECUTION_STATIC_INIT = ofMethod(ExecutionModeManager.class.getName(),
"staticInit", void.class);
public static final MethodDescriptor RUNTIME_EXECUTION_RUNTIME_INIT = ofMethod(ExecutionModeManager.class.getName(),
"runtimeInit", void.class);
public static final MethodDescriptor RUNTIME_EXECUTION_RUNNING = ofMethod(ExecutionModeManager.class.getName(),
"running", void.class);
public static final MethodDescriptor RUNTIME_EXECUTION_UNSET = ofMethod(ExecutionModeManager.class.getName(),
"unset", void.class);
public static final MethodDescriptor CONFIGURE_STEP_TIME_START = ofMethod(StepTiming.class.getName(), "configureStart",
void.class);
private static final DotName QUARKUS_APPLICATION = DotName.createSimple(QuarkusApplication.class.getName());
Expand Down Expand Up @@ -170,6 +179,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
lm);

mv.invokeStaticMethod(CONFIGURE_STEP_TIME_ENABLED);
mv.invokeStaticMethod(RUNTIME_EXECUTION_STATIC_INIT);

mv.invokeStaticMethod(ofMethod(Timing.class, "staticInitStarted", void.class, boolean.class),
mv.load(launchMode.isAuxiliaryApplication()));
Expand Down Expand Up @@ -227,6 +237,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
mv.load(i.getKey()), mv.load(i.getValue()));
}
mv.invokeStaticMethod(ofMethod(NativeImageRuntimePropertiesRecorder.class, "doRuntime", void.class));
mv.invokeStaticMethod(RUNTIME_EXECUTION_RUNTIME_INIT);

// Set the SSL system properties
if (!javaLibraryPathAdditionalPaths.isEmpty()) {
Expand Down Expand Up @@ -268,6 +279,8 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
loaders, constants, gizmoOutput, startupContext, tryBlock);
}

tryBlock.invokeStaticMethod(RUNTIME_EXECUTION_RUNNING);

// Startup log messages
List<String> featureNames = new ArrayList<>();
for (FeatureBuildItem feature : features) {
Expand Down Expand Up @@ -324,6 +337,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,

mv = file.getMethodCreator("doStop", void.class);
mv.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
mv.invokeStaticMethod(RUNTIME_EXECUTION_UNSET);
startupContext = mv.readStaticField(scField.getFieldDescriptor());
mv.invokeVirtualMethod(ofMethod(StartupContext.class, "close", void.class), startupContext);
mv.returnValue(null);
Expand Down
30 changes: 30 additions & 0 deletions core/runtime/src/main/java/io/quarkus/runtime/ExecutionMode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.runtime;

/**
* The runtime execution mode.
*/
public enum ExecutionMode {

/**
* Static initializiation.
*/
STATIC_INIT,

/**
* Runtime initialization.
*/
RUNTIME_INIT,

/**
* The application is running.
*/
RUNNING,

UNSET,
;

public static ExecutionMode current() {
return ExecutionModeManager.getExecutionMode();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.runtime;

public final class ExecutionModeManager {

private static volatile ExecutionMode executionMode = ExecutionMode.UNSET;

public static void staticInit() {
executionMode = ExecutionMode.STATIC_INIT;
}

public static void runtimeInit() {
executionMode = ExecutionMode.RUNTIME_INIT;
}

public static void running() {
executionMode = ExecutionMode.RUNNING;
}

public static void unset() {
executionMode = ExecutionMode.UNSET;
}

public static ExecutionMode getExecutionMode() {
return executionMode;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package io.quarkus.runtime.annotations;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.eclipse.microprofile.config.inject.ConfigProperty;

/**
* Used to mark a {@link org.eclipse.microprofile.config.spi.ConfigSource},
* {@link org.eclipse.microprofile.config.spi.ConfigSourceProvider} or {@link io.smallrye.config.ConfigSourceFactory}
* as safe to be initialized during STATIC INIT.
* Used to mark a configuration object as safe to be initialized during the STATIC INIT phase.
* <p>
* The target configuration objects include {@link org.eclipse.microprofile.config.spi.ConfigSource},
* {@link org.eclipse.microprofile.config.spi.ConfigSourceProvider}, {@link io.smallrye.config.ConfigSourceFactory} and
* {@link io.smallrye.config.ConfigMapping}. Moreover, this annotation can be used for
* {@link org.eclipse.microprofile.config.inject.ConfigProperty} injection points.
* <p>
*
* When a Quarkus application is starting up, Quarkus will execute first a static init method which contains some
* extensions actions and configurations. Example:
Expand All @@ -36,7 +44,7 @@
* previous code example and a ConfigSource that requires database access. In this case, it is impossible to properly
* initialize such ConfigSource, because the database services are not yet available so the ConfigSource in unusable.
*/
@Target(TYPE)
@Target({ TYPE, FIELD, PARAMETER })
@Retention(RUNTIME)
@Documented
public @interface StaticInitSafe {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public class ConfigBuildStep {
private static final Logger LOGGER = Logger.getLogger(ConfigBuildStep.class.getName());

private static final DotName MP_CONFIG = DotName.createSimple(Config.class.getName());
private static final DotName MP_CONFIG_PROPERTY_NAME = DotName.createSimple(ConfigProperty.class.getName());
static final DotName MP_CONFIG_PROPERTY_NAME = DotName.createSimple(ConfigProperty.class.getName());
private static final DotName MP_CONFIG_PROPERTIES_NAME = DotName.createSimple(ConfigProperties.class.getName());
private static final DotName MP_CONFIG_VALUE_NAME = DotName.createSimple(ConfigValue.class.getName());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.quarkus.arc.deployment;

import org.jboss.jandex.DotName;

import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.runtime.ConfigStaticInitCheck;
import io.quarkus.arc.runtime.ConfigStaticInitCheckInterceptor;
import io.quarkus.arc.runtime.ConfigStaticInitValues;
import io.quarkus.deployment.annotations.BuildStep;

public class ConfigStaticInitBuildSteps {

@BuildStep
AdditionalBeanBuildItem registerBeans() {
return AdditionalBeanBuildItem.builder()
.addBeanClasses(ConfigStaticInitCheckInterceptor.class, ConfigStaticInitValues.class,
ConfigStaticInitCheck.class)
.build();
}

@BuildStep
AnnotationsTransformerBuildItem transformConfigProducer() {
DotName configProducerName = DotName.createSimple("io.smallrye.config.inject.ConfigProducer");

return new AnnotationsTransformerBuildItem(AnnotationsTransformer.appliedToMethod().whenMethod(m -> {
// Apply to all producer methods declared on io.smallrye.config.inject.ConfigProducer
return m.declaringClass().name().equals(configProducerName)
&& m.hasAnnotation(DotNames.PRODUCES)
&& m.hasAnnotation(ConfigBuildStep.MP_CONFIG_PROPERTY_NAME);
}).thenTransform(t -> {
t.add(ConfigStaticInitCheck.class);
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.arc.test.config.staticinit;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Singleton
public class StaticInitBean {

@ConfigProperty(name = "apfelstrudel")
String value;

// bean is instantiated during STATIC_INIT
void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.quarkus.arc.test.config.staticinit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class StaticInitConfigInjectionFailureTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(StaticInitBean.class, StaticInitEagerBean.class, UnsafeConfigSource.class)
.addAsServiceProvider(ConfigSource.class, UnsafeConfigSource.class)
// the value from application.properties should be injected during STATIC_INIT
.addAsResource(new StringAsset("apfelstrudel=jandex"), "application.properties"))
.assertException(t -> {
assertThat(t).isInstanceOf(IllegalStateException.class)
.hasMessageContainingAll(
"A runtime config property value differs from the value that was injected during the static intialization phase",
"the runtime value of 'apfelstrudel' is [gizmo] but the value [jandex] was injected into io.quarkus.arc.test.config.staticinit.StaticInitBean#value",
"the runtime value of 'apfelstrudel' is [gizmo] but the value [jandex] was injected into io.quarkus.arc.test.config.staticinit.StaticInitEagerBean#value");
});

@Test
public void test() {
fail();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.arc.test.config.staticinit;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Singleton
public class StaticInitEagerBean {

@ConfigProperty(name = "apfelstrudel")
Instance<String> value;

// bean is instantiated during STATIC_INIT
void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) {
// this should trigger the failure
value.get();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.arc.test.config.staticinit;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Singleton
public class StaticInitLazyBean {

@ConfigProperty(name = "apfelstrudel")
Instance<String> value;

// bean is instantiated during STATIC_INIT
void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) {
// value is not obtained...
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.arc.test.config.staticinit;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.quarkus.runtime.annotations.StaticInitSafe;

@Singleton
public class StaticInitSafeBean {

String value;

public StaticInitSafeBean(@StaticInitSafe @ConfigProperty(name = "apfelstrudel") String value) {
this.value = value;
}

// bean is instantiated during STATIC_INIT
void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.arc.test.config.staticinit;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class StaticInitSafeConfigInjectionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(StaticInitSafeBean.class, StaticInitLazyBean.class, UnsafeConfigSource.class)
.addAsServiceProvider(ConfigSource.class, UnsafeConfigSource.class)
// the value from application.properties should be injected during STATIC_INIT
.addAsResource(new StringAsset("apfelstrudel=jandex"), "application.properties"));

@Inject
StaticInitSafeBean bean;

@Test
public void test() {
assertEquals("jandex", bean.value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.arc.test.config.staticinit;

import java.util.Set;

import org.eclipse.microprofile.config.spi.ConfigSource;

// Intentionally not annotated with @StaticInitSafe so that it's not considered durin the STATIC_INIT
public class UnsafeConfigSource implements ConfigSource {

@Override
public Set<String> getPropertyNames() {
return Set.of("apfelstrudel");
}

@Override
public String getValue(String propertyName) {
return propertyName.equals("apfelstrudel") ? "gizmo" : null;
}

@Override
public String getName() {
return "Unsafe Test";
}

@Override
public int getOrdinal() {
return 500;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public Object create(CreationalContext<Object> creationalContext, Map<String, Ob
throw new IllegalStateException("No current injection point found");
}

ConfigStaticInitCheckInterceptor.recordConfigValue(injectionPoint, null);

try {
return ConfigProducerUtil.getValue(injectionPoint, ConfigProvider.getConfig());
} catch (Exception e) {
Expand Down
Loading

0 comments on commit 46febda

Please sign in to comment.