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

Introduce behavior-change configs as an alternative to feature configs #1124

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

eric-maynard
Copy link
Contributor

Polaris currently has the PolarisConfiguration type as a catch-all for behavior that can be altered via the application.properties. When I refactored it in #97 I imagined that we would only use these flags for "features" -- things a typical user will want to enable/disable depending on their deployment, and things that the project should support for a long time.

It's becoming apparent that we'll also want to use flags to protect behavior changes that aren't strictly features. For example, a bugfix that may cause a regression for users relying on buggy behavior, or a controversial change like the one in #490 where theoretical performance concerns could cause a regression.

To better support these cases, this PR refactors PolarisConfiguration into FeatureConfiguration and BehaviorChangeConfiguration. In the future, changes that are perceived to be high-risk or to have a high chance to cause a regression should be gated behind behavior change flags.

@eric-maynard
Copy link
Contributor Author

It seems that there's some conflict between the two ConfigMappings that Quarkus is not able to resolve. Currently, tests that rely on configOverrides do not pass. If I comment out the new mapping, Quarkus tests pass as expected:

@StaticInitSafe
// @ConfigMapping(prefix = "polaris.behavior-changes")
public interface QuarkusBehaviorChangesConfiguration extends FeaturesConfiguration {

@eric-maynard
Copy link
Contributor Author

For now I've found that duplicating code between FeatureConfiguration and QuarkusBehaviorChangeConfiguration allows Quarkus to parse the configs correctly, though ideally QuarkusBehaviorChangeConfiguration should just extend FeatureConfiguration. Added a comment to this effect.

@dimas-b
Copy link
Contributor

dimas-b commented Mar 6, 2025

Could we quickly discuss this in the community sync tomorrow?

@eric-maynard
Copy link
Contributor Author

Good idea @dimas-b, let's plan to discuss there

@adutra
Copy link
Contributor

adutra commented Mar 6, 2025

My general feeling about FeatureConfiguration is that it's too schemaless. I don't think this PR addresses that, but I think it's something that needs to be addressed soon.

A typical configuration class in a Quarkus application would like like this:

@ConfigMapping(prefix = "polaris.feature")
interface FeatureConfiguration {
  boolean configX();
  Optional<String> configY();
  List<String> configZ();
}

However, we currently have this:

@ConfigMapping(prefix = "polaris.feature")
interface FeatureConfiguration {
  Map<String, String> defaults();
}

Where the map values are dynamically parsed as json, then each map entry is collated against the fields in PolarisConfiguration:

public class PolarisConfiguration<T> {
  public static final PolarisConfiguration<Boolean> CONFIG_X = ...
  public static final PolarisConfiguration<String> CONFIG_Y = ...
  public static final PolarisConfiguration<List<String>> CONFIG_Z = ...
}

This makes the code quite brittle, as it's not possible to statically compile it against an existing method like FeatureConfiguration.configX().

I understand that the goal is to be able to override things on different levels, by realm for instance – and I think that's a great idea. But there are other ways to achieve that while preserving a strongly-typed configuration mechanism.

Let's discuss this at the next meeting, but I would suggest to refactor this by grouping the configuration options into logical groups inside one or many configuration classes.

@eric-maynard
Copy link
Contributor Author

@adutra I agree there. Actually, I am hoping that when we refactor FeatureConfiguration to make it more structured, we can also resolve the issue where QuarkusBehaviorChangeConfiguration and QuarkusFeatureConfiguration don't share a type. I also think it's a little odd that we have the logic sort of split between e.g. PolarisFeatureConfiguration and then QuarkusFeatureConfiguration, so perhaps we can fix that as well.

With all that being said, I see adding a new type of config as not strictly related to these changes. Potentially, we could establish the existence of these configs here and then later refactor the FeatureConfiguration types

@adutra
Copy link
Contributor

adutra commented Mar 6, 2025

I also think it's a little odd that we have the logic sort of split between e.g. PolarisFeatureConfiguration and then QuarkusFeatureConfiguration, so perhaps we can fix that as well.

That split is rather for technical reasons, since polaris-service-common does not depend on Quarkus at all. But I'm open to revisit that as well.

@eric-maynard
Copy link
Contributor Author

since polaris-service-common does not depend on Quarkus at all. But I'm open to revisit that as well.

I see. Yeah, I am also open to revisiting that if it simplifies things.

For now, what do you think is the best way to proceed? Should we introduce the flags with something like this change, and then try to refactor PolarisConfiguration etc. after? I'm a little worried about trying to sort out the refactor now (especially something potentially controversial like adding a Quarkus dependency in polaris-sevice-common) since other PRs like #1068 that should be gated behind a flag will be blocked.

*
* @param <T> The type of the configuration
*/
public class BehaviorChangeConfiguration<T> extends PolarisConfiguration<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need different types to support this? I think probably adding a field to PolarisConfiguration would be sufficient

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Strictly speaking we don't need a new type, but I chose to add one because I think it's valuable to separate the actual config instances from one another. Besides just having them clearly separated & organized, it means the caller is forced to recognize which type they are relying on (e.g. BehaviorChangeConfiguration.UNSTABLE_FLAG vs FeatureConfiguration.ACTUAL_FEATURE). It also allows us to override e.g. catalogConfig which shouldn't be valid for these flags.

Copy link
Contributor

Choose a reason for hiding this comment

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

It also allows us to override e.g. catalogConfig which shouldn't be valid for these flags.

That seems reasonable, but given that we use a builder, can't we just enforce that at construction? I do like having the different config types housed in different namespaces (as in your examples BehaviorChangeConfiguration.UNSTABLE_FLAG vs FeatureConfiguration.ACTUAL_FEATURE)...

I guess I would just like to ensure that the call sites that are checking for config values don't need to know what type the configuration is at runtime. I can just imagine cases where someone accidentally references one type and the config field is another or someone tries to write some generic code that only works for one config type but not the other...

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

Successfully merging this pull request may close these issues.

4 participants