From 81e35801ef16b0a7f15a3549d76c9f7b52425823 Mon Sep 17 00:00:00 2001 From: Gwenneg Lepage Date: Mon, 4 Mar 2024 10:18:58 +0100 Subject: [PATCH] Allow deactivating the extension at runtime --- .../quarkiverse/unleash/UnleashProcessor.java | 22 ++-- .../quarkiverse/unleash/NoOpUnleashTest.java | 122 ++++++++++++++++++ .../ROOT/pages/includes/quarkus-unleash.adoc | 17 +++ .../runtime/AbstractVariantProducer.java | 4 +- .../unleash/runtime/NoOpUnleash.java | 86 ++++++++++++ .../runtime/UnleashLifecycleManager.java | 24 ++++ .../unleash/runtime/UnleashRecorder.java | 40 +++++- .../runtime/UnleashRuntimeTimeConfig.java | 6 + .../unleash/runtime/UnleashService.java | 52 -------- 9 files changed, 307 insertions(+), 66 deletions(-) create mode 100644 deployment/src/test/java/io/quarkiverse/unleash/NoOpUnleashTest.java create mode 100644 runtime/src/main/java/io/quarkiverse/unleash/runtime/NoOpUnleash.java create mode 100644 runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashLifecycleManager.java delete mode 100644 runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashService.java diff --git a/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java b/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java index 73dd2c0..d398a83 100644 --- a/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/unleash/UnleashProcessor.java @@ -24,20 +24,23 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.pkg.steps.NativeBuild; -import io.quarkus.gizmo.*; -import io.quarkus.runtime.ApplicationConfig; -import io.quarkus.runtime.LaunchMode; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldCreator; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; import io.quarkus.runtime.util.HashUtil; public class UnleashProcessor { @@ -55,9 +58,12 @@ public class UnleashProcessor { @BuildStep @Record(RUNTIME_INIT) - void configureRuntimeProperties(UnleashRecorder recorder, UnleashRuntimeTimeConfig runtimeConfig, - ApplicationConfig appConfig, LaunchModeBuildItem launchMode) { - recorder.initializeProducers(runtimeConfig, appConfig, launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT); + SyntheticBeanBuildItem createUnleashSyntheticBean(UnleashRecorder unleashRecorder) { + return SyntheticBeanBuildItem.configure(Unleash.class) + .scope(Singleton.class) + .supplier(unleashRecorder.getSupplier()) + .setRuntimeInit() + .done(); } @BuildStep(onlyIf = NativeBuild.class) @@ -103,7 +109,7 @@ NativeImageConfigBuildItem buildNativeImage() { AdditionalBeanBuildItem additionalBeans() { return AdditionalBeanBuildItem.builder() .setUnremovable() - .addBeanClasses(UnleashService.class, FeatureToggle.class, FeatureToggleProducer.class, + .addBeanClasses(UnleashLifecycleManager.class, FeatureToggle.class, FeatureToggleProducer.class, UnleashResourceProducer.class, ToggleVariantProducer.class, ToggleVariantStringProducer.class) .build(); } diff --git a/deployment/src/test/java/io/quarkiverse/unleash/NoOpUnleashTest.java b/deployment/src/test/java/io/quarkiverse/unleash/NoOpUnleashTest.java new file mode 100644 index 0000000..bbbcb63 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/unleash/NoOpUnleashTest.java @@ -0,0 +1,122 @@ +package io.quarkiverse.unleash; + +import static org.junit.jupiter.api.Assertions.*; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.getunleash.Unleash; +import io.getunleash.Variant; +import io.quarkiverse.unleash.runtime.NoOpUnleash; +import io.quarkus.test.QuarkusUnitTest; + +public class NoOpUnleashTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.unleash.active=false"), "application.properties") + .addClass(TestBean.class)); + + @Inject + TestBean testBean; + + @Test + void testInjectedImplementation() { + assertEquals(NoOpUnleash.class, testBean.getUnleash().getClass()); + } + + @Test + void testFeatureToggle() { + assertFalse(testBean.getAlpha().get()); + assertTrue(testBean.getBravo().get()); + } + + @Test + void testFeatureVariant() { + assertNull(testBean.getCharlie().get()); + assertNull(testBean.getDelta().get()); + assertNull(testBean.getEcho().get()); + } + + @Test + void testUnleashValues() { + assertFalse(testBean.isEnabled("foxtrot")); + assertTrue(testBean.isEnabled("golf", true)); + assertNull(testBean.getVariant("hotel")); + Variant variant = new Variant("india", "payload", true); + assertEquals(variant, testBean.getVariant("india", variant)); + } + + @ApplicationScoped + static class TestBean { + + @FeatureToggle(name = "alpha") + Instance alpha; + + @FeatureToggle(name = "bravo", defaultValue = true) + Instance bravo; + + @FeatureVariant(name = "charlie") + Instance charlie; + + @FeatureVariant(name = "delta") + Instance delta; + + @FeatureVariant(name = "echo") + Instance echo; + + @Inject + Unleash unleash; + + public Instance getAlpha() { + return alpha; + } + + public Instance getBravo() { + return bravo; + } + + public Instance getCharlie() { + return charlie; + } + + public Instance getDelta() { + return delta; + } + + public Instance getEcho() { + return echo; + } + + public Unleash getUnleash() { + return unleash; + } + + public boolean isEnabled(String toggleName) { + return unleash.isEnabled(toggleName); + } + + public boolean isEnabled(String toggleName, boolean defaultSetting) { + return unleash.isEnabled(toggleName, defaultSetting); + } + + public Variant getVariant(String toggleName) { + return unleash.getVariant(toggleName); + } + + public Variant getVariant(String toggleName, Variant defaultValue) { + return unleash.getVariant(toggleName, defaultValue); + } + } + + public static class Param { + public String text; + public Long value; + public Boolean enabled; + } +} diff --git a/docs/modules/ROOT/pages/includes/quarkus-unleash.adoc b/docs/modules/ROOT/pages/includes/quarkus-unleash.adoc index 4107c5b..4ae1f7f 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-unleash.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-unleash.adoc @@ -192,6 +192,23 @@ endif::add-copy-button-to-env-var[] |`unleash-db` +a| [[quarkus-unleash_quarkus-unleash-active]]`link:#quarkus-unleash_quarkus-unleash-active[quarkus.unleash.active]` + + +[.description] +-- +Whether or not the Unleash extension is active. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_UNLEASH_ACTIVE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_UNLEASH_ACTIVE+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`true` + + a| [[quarkus-unleash_quarkus-unleash-url]]`link:#quarkus-unleash_quarkus-unleash-url[quarkus.unleash.url]` diff --git a/runtime/src/main/java/io/quarkiverse/unleash/runtime/AbstractVariantProducer.java b/runtime/src/main/java/io/quarkiverse/unleash/runtime/AbstractVariantProducer.java index 7d00d02..ef4896c 100644 --- a/runtime/src/main/java/io/quarkiverse/unleash/runtime/AbstractVariantProducer.java +++ b/runtime/src/main/java/io/quarkiverse/unleash/runtime/AbstractVariantProducer.java @@ -35,7 +35,7 @@ protected Variant getVariant(InjectionPoint injectionPoint, Unleash unleash) { protected String getVariantString(InjectionPoint injectionPoint, Unleash unleash) { Variant variant = getVariant(injectionPoint, unleash); - if (!variant.isEnabled()) { + if (variant == null || !variant.isEnabled()) { return null; } Optional payload = variant.getPayload(); @@ -47,7 +47,7 @@ protected Object getVariantJsonObject(InjectionPoint injectionPoint, Class cl FeatureVariant ft = getFeatureVariant(injectionPoint); Variant variant = unleash.getVariant(ft.name()); - if (!variant.isEnabled()) { + if (variant == null || !variant.isEnabled()) { return null; } Optional tmp = variant.getPayload(); diff --git a/runtime/src/main/java/io/quarkiverse/unleash/runtime/NoOpUnleash.java b/runtime/src/main/java/io/quarkiverse/unleash/runtime/NoOpUnleash.java new file mode 100644 index 0000000..9e03c59 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/unleash/runtime/NoOpUnleash.java @@ -0,0 +1,86 @@ +package io.quarkiverse.unleash.runtime; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.BiPredicate; + +import io.getunleash.EvaluatedToggle; +import io.getunleash.FeatureToggle; +import io.getunleash.MoreOperations; +import io.getunleash.Unleash; +import io.getunleash.UnleashContext; +import io.getunleash.Variant; + +public class NoOpUnleash implements Unleash { + + @Override + public boolean isEnabled(String toggleName, UnleashContext context, BiPredicate fallbackAction) { + if (fallbackAction != null) { + return fallbackAction.test(toggleName, context); + } else { + return false; + } + } + + @Override + public Variant getVariant(String toggleName, UnleashContext context) { + return null; + } + + @Override + public Variant getVariant(String toggleName, UnleashContext context, Variant defaultValue) { + return defaultValue; + } + + @Override + public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { + return null; + } + + @Override + public Variant deprecatedGetVariant(String toggleName, UnleashContext context, Variant defaultValue) { + return defaultValue; + } + + @Override + public List getFeatureToggleNames() { + return Collections.emptyList(); + } + + @Override + public MoreOperations more() { + return more; + } + + private final MoreOperations more = new MoreOperations() { + + @Override + public List getFeatureToggleNames() { + return Collections.emptyList(); + } + + @Override + public Optional getFeatureToggleDefinition(String toggleName) { + return Optional.empty(); + } + + @Override + public List evaluateAllToggles() { + return Collections.emptyList(); + } + + @Override + public List evaluateAllToggles(UnleashContext context) { + return Collections.emptyList(); + } + + @Override + public void count(String toggleName, boolean enabled) { + } + + @Override + public void countVariant(String toggleName, String variantName) { + } + }; +} diff --git a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashLifecycleManager.java b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashLifecycleManager.java new file mode 100644 index 0000000..b479a7f --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashLifecycleManager.java @@ -0,0 +1,24 @@ +package io.quarkiverse.unleash.runtime; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.Shutdown; +import jakarta.enterprise.event.Startup; + +import io.getunleash.Unleash; +import io.quarkus.logging.Log; + +public class UnleashLifecycleManager { + + // This method is used to eagerly create the Unleash bean instance at RUNTIME_INIT execution time. + void onStartup(@Observes Startup event, Unleash unleash) { + unleash.more(); + } + + void onShutdown(@Observes Shutdown event, Unleash unleash) { + try { + unleash.shutdown(); + } catch (Exception ex) { + Log.error("Shutdown unleash client failed!", ex); + } + } +} diff --git a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRecorder.java b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRecorder.java index b9995a7..a672580 100644 --- a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRecorder.java +++ b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRecorder.java @@ -1,15 +1,47 @@ package io.quarkiverse.unleash.runtime; -import io.quarkus.arc.Arc; +import java.util.function.Supplier; + +import org.jboss.logging.Logger; + +import io.getunleash.Unleash; import io.quarkus.runtime.ApplicationConfig; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @Recorder public class UnleashRecorder { - public void initializeProducers(UnleashRuntimeTimeConfig config, ApplicationConfig appConfig, boolean devMode) { - UnleashService producer = Arc.container().instance(UnleashService.class).get(); - producer.initialize(config, appConfig, devMode); + private static final Logger LOGGER = Logger.getLogger(UnleashRecorder.class); + + private final ApplicationConfig applicationConfig; + private final RuntimeValue unleashRuntimeConfig; + + public UnleashRecorder(ApplicationConfig applicationConfig, RuntimeValue unleashRuntimeConfig) { + this.applicationConfig = applicationConfig; + this.unleashRuntimeConfig = unleashRuntimeConfig; } + public Supplier getSupplier() { + if (unleashRuntimeConfig.getValue().active) { + String app = unleashRuntimeConfig.getValue().appName.orElse(applicationConfig.name.orElse("default-app-name")); + return new Supplier() { + @Override + public Unleash get() { + Unleash unleash = UnleashCreator.createUnleash(unleashRuntimeConfig.getValue(), app); + LOGGER.infof("Unleash client application '{}' fetch feature toggle names: {}", app, + unleash.more().getFeatureToggleNames()); + return unleash; + } + }; + } else { + return new Supplier() { + @Override + public Unleash get() { + LOGGER.info("Unleash client is disabled from the extension configuration"); + return new NoOpUnleash(); + } + }; + } + } } diff --git a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRuntimeTimeConfig.java b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRuntimeTimeConfig.java index 9dad7a9..3fdb8f6 100644 --- a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRuntimeTimeConfig.java +++ b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashRuntimeTimeConfig.java @@ -9,6 +9,12 @@ @ConfigRoot(name = "unleash", phase = ConfigPhase.RUN_TIME) public class UnleashRuntimeTimeConfig { + /** + * Whether or not the Unleash extension is active. + */ + @ConfigItem(defaultValue = "true") + public boolean active; + /** * Unleash URL service endpoint */ diff --git a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashService.java b/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashService.java deleted file mode 100644 index 2fe175e..0000000 --- a/runtime/src/main/java/io/quarkiverse/unleash/runtime/UnleashService.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.quarkiverse.unleash.runtime; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Produces; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.getunleash.Unleash; -import io.quarkus.logging.Log; -import io.quarkus.runtime.ApplicationConfig; -import io.quarkus.runtime.ShutdownEvent; - -@ApplicationScoped -public class UnleashService { - - private static final Logger log = LoggerFactory.getLogger(UnleashService.class); - - static Unleash client; - - boolean devMode; - - void initialize(UnleashRuntimeTimeConfig config, ApplicationConfig appConfig, boolean devMode) { - this.devMode = devMode; - if (devMode && client != null) { - return; - } - String app = config.appName.orElse(appConfig.name.orElse("default-app-name")); - client = UnleashCreator.createUnleash(config, app); - - log.info("Unleash client application '{}' fetch feature toggle names: {}", app, - client.more().getFeatureToggleNames()); - } - - void onStop(@Observes ShutdownEvent event) { - if (devMode) { - return; - } - try { - client.shutdown(); - } catch (Exception ex) { - Log.error("Shutdown unleash client failed!", ex); - } - } - - @Produces - public Unleash client() { - return client; - } - -}