Hi everyone,
On @geoand's suggestion, I'm starting this conversation to talk about
quarkus#28032 which is an extension proposal to introduce lightweight feature flags in Quarkus. To save you from switching between this conversation and #28032, I'll put here everything I already wrote in #28032. This post will contain ideas that are all open for suggestions and opinions. Some of them may not be compatible with each other.
Why lightweight? Because the extension wouldn't rely on any external feature flags library/framework/server, unlike
quarkus-unleash or any other similar extension.
Why would a project depend on lightweight feature flags instead of
Unleash (or similar)? Well, Unleash is an amazing tool but it also comes with some constraints that may not be acceptable for all projects, such as deploying and maintaining the Unleash server and its underlying DB for example. Projects that won't use Unleash because of these constraints could benefit from a simpler feature flags implementation.
I'm part of a
console.redhat.com team that uses a lot of feature flags for several reasons, including:
- we often need to merge parts of unfinished huge features that must remain disabled until everything's ready for production
- we have to deal with risky migrations that sometimes affect the entire app and require extra precautions (intensive testing, automated and manual) before we actually migrate the production data
Our feature flags are custom-made and in-memory. At first, I wanted to write a Quarkus blog post to show how we use these flags and how we run our tests with any flags combinations very easily. Then I realized this could actually be the base of a new Quarkus extension.
Here's how things could look like from a user perspective if I transformed our custom-made feature flags into a Quarkus extension:
import quarkus.featureflags.FeatureFlags;
@FeatureFlags
public class FeatureFlipper {
@ConfigProperty(name = "huge-feature.enabled", defaultValue = "true")
boolean hugeFeatureEnabled;
@ConfigProperty(name = "risky-feature.enabled", defaultValue = "false")
Boolean riskyFeatureEnabled;
/* getters and setters */
}
@ApplicationScoped
public class MyService {
@Inject
FeatureFlipper featureFlipper;
public void doSomething() {
if (featureFlipper.isHugeFeatureEnabled()) {
// Do something with the huge feature
} else {
// Do something without the huge feature, or even nothing at all
}
}
}
Now, let's talk a bit about implementation details.
1. The @FeatureFlags annotation could only be allowed on a single class in each Quarkus app, because it's a good practice to centralize all flags at the same place for easier maintenance. Build time validation would throw a DeploymentException if multiple instances of that annotation were detected. Adding that annotation to a class would make it a @Singleton bean using BeanDefiningAnnotationBuildItem at build time.
2. Each boolean or Boolean @ConfigProperty field from the annotated class would automatically be considered as a feature flag. A default value could be mandatory for these fields and a DeploymentException could be thrown at build time when it is missing.
3. Flags values could be changed from:
- the usual MP Config way of setting configuration values (application.properties, environment variables and all other kinds of config sources)
- calling setters on the feature flags boolean fields from the app (test) code
- the dev console, in order to test and enable/disable features in dev mode without having to reload the app at all
Another way of changing the flags values in real-time could be through a REST API but it may be better to document that and not provide it as an extension feature for security reasons. Users would use examples from our doc to build their REST API while being in control of the security associated with the API.
4. The extension config could contain a quarkus.feature-flags.log-at-startup=true|false (default true) config key to determine whether the flags values should be logged at app startup or not. This is something we use in my team and it can be really helpful when something goes wrong on prod and we need to know which flags are enabled.
5. The extension config could contain a quarkus.feature-flags.expose-flags=true|false (default false) config key to expose all feature flags through an HTTP API. This can be really helpful when flags managed from a backend app are consumed by a frontend app to determine which UI features should be enabled. The HTTP endpoint path would start with the "non application root path" (/q) like metrics, health and friends.
6. @geoand added this comment in #28032:
"One thing I would like to figure out in the aforementioned discussion is how much it would make sense to have a declarative first approach - where classes and/or methods are swapped based on the presence of a feature set, using annotations and config. The idea being that method bodies should be unaffected."
I started working on an extension POC about this:
quarkus-feature-flag. Feel free to take a look at it!
That POC contains the @FeatureFlags annotation and identifies the features flags fields at build time using Jandex. It logs the flags at startup using generated bytecode (with Gyzmo) and a synthetic observer (thanks @mkouba for helping me with this!). It exposes the flags using generated bytecode (Gyzmo again) and an HTTP endpoint at /q/featureflags using a Vert.x route (resteasy is not required). This is only the beginning but it already covers most of the things that we use in our apps at
console.redhat.com.
One major aspect of our custom-made features flags is the flexibility they offer during the tests execution. Our flags can be overridden from the test code at any time (before, after or in the middle of a test). We simply check that the active Quarkus profile is a test profile before allowing the flags overrides. That kind of flexibility cannot be achieved while setting system properties from tests or using Quarkus test profiles, which is why we implemented our own feature flags in the first place. There's no rocket science behind this, as we simply rely on Java boolean fields and change their values at run time. However, I think it'd be great to have a section in the extension doc that would explain how to run tests with feature flags efficiently.
Now I'd like to know what you think about this :)
Thanks for reading this!
Gwenneg