Skip to content

Commit

Permalink
Allow deactivating the extension at runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
gwenneg committed Mar 4, 2024
1 parent 28f2bc1 commit 81e3580
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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();
}
Expand Down
122 changes: 122 additions & 0 deletions deployment/src/test/java/io/quarkiverse/unleash/NoOpUnleashTest.java
Original file line number Diff line number Diff line change
@@ -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<Boolean> alpha;

@FeatureToggle(name = "bravo", defaultValue = true)
Instance<Boolean> bravo;

@FeatureVariant(name = "charlie")
Instance<String> charlie;

@FeatureVariant(name = "delta")
Instance<Variant> delta;

@FeatureVariant(name = "echo")
Instance<Param> echo;

@Inject
Unleash unleash;

public Instance<Boolean> getAlpha() {
return alpha;
}

public Instance<Boolean> getBravo() {
return bravo;
}

public Instance<String> getCharlie() {
return charlie;
}

public Instance<Variant> getDelta() {
return delta;
}

public Instance<Param> 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;
}
}
17 changes: 17 additions & 0 deletions docs/modules/ROOT/pages/includes/quarkus-unleash.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]`


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> payload = variant.getPayload();
Expand All @@ -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<Payload> tmp = variant.getPayload();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, UnleashContext> 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<String> getFeatureToggleNames() {
return Collections.emptyList();
}

@Override
public MoreOperations more() {
return more;
}

private final MoreOperations more = new MoreOperations() {

@Override
public List<String> getFeatureToggleNames() {
return Collections.emptyList();
}

@Override
public Optional<FeatureToggle> getFeatureToggleDefinition(String toggleName) {
return Optional.empty();
}

@Override
public List<EvaluatedToggle> evaluateAllToggles() {
return Collections.emptyList();
}

@Override
public List<EvaluatedToggle> evaluateAllToggles(UnleashContext context) {
return Collections.emptyList();
}

@Override
public void count(String toggleName, boolean enabled) {
}

@Override
public void countVariant(String toggleName, String variantName) {
}
};
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UnleashRuntimeTimeConfig> unleashRuntimeConfig;

public UnleashRecorder(ApplicationConfig applicationConfig, RuntimeValue<UnleashRuntimeTimeConfig> unleashRuntimeConfig) {
this.applicationConfig = applicationConfig;
this.unleashRuntimeConfig = unleashRuntimeConfig;
}

public Supplier<Unleash> getSupplier() {
if (unleashRuntimeConfig.getValue().active) {
String app = unleashRuntimeConfig.getValue().appName.orElse(applicationConfig.name.orElse("default-app-name"));
return new Supplier<Unleash>() {
@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<Unleash>() {
@Override
public Unleash get() {
LOGGER.info("Unleash client is disabled from the extension configuration");
return new NoOpUnleash();
}
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading

0 comments on commit 81e3580

Please sign in to comment.