Skip to content

Conversation

@andponlin
Copy link
Contributor

The config system currently obtains fallback values from system properties and env-vars. This is not always desirable. This PR provides a way to make the fallbacks pluggable so that a project can opt to provide a fallback in a different way.

- rename ConfigurationFallbacks to ConfigurationFallback
- Change List<ConfigurationFallbacks> fallbacks to single ConfigurationFallback fallback
- Make DefaultFallback final
- Rename method Configuration.Builder.withFallbacks() to fallback()
@rbygrave
Copy link
Contributor

rbygrave commented Dec 6, 2025

Hi @andponlin I've push a commit that changes this from using a List<ConfigurationFallbacks> to a single ConfigurationFallback (plus some refactor renames).

Can you review those changes I've made and see if you are happy with them? Thanks, Rob.

} else if (spi instanceof ModificationEventRunner) {
_eventRunner = (ModificationEventRunner) spi;
} else if (spi instanceof ConfigurationFallback) {
_fallback = (ConfigurationFallback) spi;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's probably a good idea to either fail if there are two instances of ConfigurationFallback configured; otherwise the behaviour may become unpredictable.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I have a use case where for Tests, it would be useful to use an alternative ConfigurationFallback.

In this case the test configuration would have values like ssm://test/myapp/some_key_of_secret

... and if a value had that special prefix it would load the value from the AWS secret store. You need aws single signon to be able to do this of course. This is to assist 2 scenarios which is (1) running locally against Dev/Test and (2) Integration tests

I think the override feature would actually support this and ideally this is only done for Testing, so the override implementation is only in maven test scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rbygrave ; I think it may be good to look into throwing in this scenario because otherwise the behaviour could be confusing and difficult to track down.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I have a use case where for Tests, it would be useful to use an alternative ConfigurationFallback.

So in this case there would be an instance of ConfigurationFallback from the main classpath and an instance from the test classpath? It may be worth logging to stderr (at least) that this is happening because in other situation a clash will be hard to comprehend.

@andponlin
Copy link
Contributor Author

Can you review those changes I've made and see if you are happy with them? Thanks, Rob.

Afternoon @rbygrave ; thanks for the review on this and totally happy with the change to have a single ConfigurationFallback configured. I have made another sugestion here but otherwise all good to me. 👍

@rbygrave
Copy link
Contributor

rbygrave commented Dec 7, 2025

currently obtains fallback values from system properties and env-vars. This is not always desirable.

What is the actual use case @andponlin for this? What case are we hitting where the current behaviour isn't desirable and what are we replacing the system properties/env vars with in that case?

@andponlin
Copy link
Contributor Author

What case are we hitting where the current behaviour isn't desirable and what are we replacing the system properties/env vars with in that case?

What I would ultimately like to achieve is that env-vars and system properties (in that order) are overrides for config keys and not the other way around such that they are fallbacks. I want to be able to override a value via env-var regardless if it is configured via configuration files or not. I also want to use a different mapping from the configuration key to the env-var name.

In my case, I would implement ConfigurationFallback to prevent a fallback being possible at all; it always returns null. I initially implement this PR as a flag to disable fallbacks but then maybe some people would prefer to have fallbacks for just the system property or just env-vars or maybe they have some other source of fallbacks or maybe they want to read from a differently formatted env-var name. This means with the ConfigurationFallback is more flexible and general for handling such cases.

To implement the override behaviour, I am currently implementing ConfigurationPlugin and then iterating the keys and replacing the values but this is not working if there is a configuration key for which there is no values configured in a file. I may need to likewise suggest an SPI interface to be able to allow for dynamic overrides.

Does this make sense? 🤔

@SentryMan
Copy link
Collaborator

What I would ultimately like to achieve is that env-vars and system properties (in that order) are overrides for config keys and not the other way around such that they are fallbacks. I want to be able to override a value via env-var regardless if it is configured via configuration files or not. I also want to use a different mapping from the configuration key to the env-var name.

To borrow a phrase from my stance on social media, "I don't follow". How does disabling fallback translate to env variables being treated as the override. Is it possible to provide a more concrete example?

@rbygrave
Copy link
Contributor

rbygrave commented Dec 7, 2025

ultimately like to achieve is that env-vars and system properties (in that order) are overrides for config keys

Right, makes sense.

In my case, I would implement ConfigurationFallback to prevent a fallback being possible at all;

Yeah.

My gut is suggesting that this PR doesn't actually resolve the issue for your use case? The use case of ... "env-vars and system properties (in that order) are overrides" ... actually needs a different PR in order to achieve that ... and then if we do that do we actually need this pluggable ConfigurationFallback? Maybe but maybe not?

Seems to me we should be looking at the source issue of supporting "env-vars and system properties (in that order) are overrides" first, and then seeing where that gets us. We might still want this pluggable ConfigurationFallback but we should probably solve the other issue first to make sure this still makes sense? Or said differently, I'd feel more comfortable leaving this PR un-merged until we've sorted the "overrides" issue just to make sure it still makes sense?

@SentryMan
Copy link
Collaborator

To implement the override behaviour, I am currently implementing ConfigurationPlugin and then iterating the keys and replacing the values but this is not working if there is a configuration key for which there is no values configured in a file.

Another approach might be to wrap the Config class in a wrapper that checks the env variable first before calling Config.get

public class ConfigWrap {
  public static String get(String key) {
    String override = deriveOverride(key);

    return override != null ? override : Config.get(key);
  }

  private static String deriveOverride(String key) {
    String systemPropertiesValue = System.getProperty(key);

    if (null != systemPropertiesValue) {
      return systemPropertiesValue;
    }

    return System.getenv(mapKeyToEnvVar(key));
  }

  private static String mapKeyToEnvVar(String key) {
    key = key.replace(".", "_");
    key = key.replace("-", "");
    return key.toUpperCase();
  }
}

@rbygrave
Copy link
Contributor

rbygrave commented Dec 8, 2025

A potential alternative is to modify the interface Configuration.ExpressionEval. Currently this only takes the value and doesn't also yet take the key ... but IF it also took the key and became String eval(String key, String value) ... and that was an implementation that was a plugin ...

... and a variation is to put a change into ConfigurationCore before (and after?) the call to eval(String) [and I'll have a play there ...]

@andponlin
Copy link
Contributor Author

Is it possible to provide a more concrete example?

  1. Consider a config key hds.tool.convert.limit-memory
  2. optionally override hds.tool.convert.limit-memory-s configured value with
    1. an env-var HDS_TOOL_CONVERT_LIMITMEMORY
    2. a system property hds.tool.convert.limit-memory
  3. optionally supply the key's value with the regular configuration file(s) (as-built)
  4. no fallback

My gut is suggesting that this PR doesn't actually resolve the issue for your use case?

Yes you're right Rob, this PR doesn't achieve the whole desired outcome. It enables only the last bit. The rest would be a follow-on.

Another approach might be to wrap the Config class in a wrapper that checks the env variable first before calling Config.get
A potential alternative is to modify the interface Configuration.ExpressionEval.

OK, I think that would be a good extension point that sounds as if it would allow for bespoke overrides.

- add overrideValue() method
- Invoke overrideValue() on initial load and builder load
@rbygrave
Copy link
Contributor

rbygrave commented Dec 8, 2025

I've pushed a change that adds a overrideValue() method to ConfigurationFallback, and invokes it on initial load and build.

Note that ConfigurationSource plugins like the AWS AppConfig one actually bypass the overrideValue() method. That is, the ConfigrationCode.applyChanges() method it doesn't use the fallback instance (but it could, hmm, pondering this).

In theory this gives us the options we need for this use case (A) control overriding the values (B) control fallback value

* @param source The source of the key value pair
*/
default String overrideValue(String key, String value, String source) {
return value;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

nit: I guess the implementation of ConfigurationFallback is not able to manipulate the source as well here? That would be a nice addition if it could but likely involves returning a record, CoreEntry or similar. Maybe not worth it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Pushed a commit that returns Entry, allows us to override the source along with the value.

@rbygrave
Copy link
Contributor

rbygrave commented Dec 8, 2025

limit-memory ... hypens

According to my research, hypens are not allowed in POSIX Standard environment variable names. So we could provide a helper method that does the env var name conversion properly including the trimming out of - chars (and treat the lack of that in the current implementation as a bug).

@andponlin
Copy link
Contributor Author

According to my research, hypens are not allowed in POSIX Standard environment variable names.

In the example above, I'm mapping hds.tool.convert.limit-memory (config key / system property) to HDS_TOOL_CONVERT_LIMITMEMORY (env-var) with the hyphen removed. See what SpringBoot is doing in this regard.

This allows us to override the source along with the value.
A downside is that we need to expose more methods on the
Entry interface that were otherwise internal to CoreEntry.
@rbygrave
Copy link
Contributor

rbygrave commented Dec 8, 2025

(env-var) with the hyphen removed. See what SpringBoot is doing in this regard.

Yes. I think we should also trim the hypens and make that change as a [separate] bug fix.

Rather than nullable, and thus allow the source to also be provided by
the fallback method.
@rbygrave rbygrave requested a review from SentryMan December 8, 2025 18:59
import java.util.Optional;

/**
* Implementations of this class are able to define a fallback value when there is no
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am wondering if the name ConfigurationFallback is the best since it has a dual purpose of providing a fallback and an overrride. Maybe ConfigurationValueInterceptor or ConfigurationOverrideAndFallback might be clearer?

Even if this name is retained as-is, the class comments would be good to update so that they describe the role of the SPI interface.

@SentryMan
Copy link
Collaborator

perhaps instead of a new fallback interface, we should change the behavior in general to prefer system/env variables over configuration files. That is, if a system property/env is set, prefer that over all else

@rbygrave
Copy link
Contributor

rbygrave commented Dec 8, 2025

Ok, I think I'm happy with this change.

In doing this, I have the thought that the behaviour being asked for is actually consistent with how Spring does it [I will see if I can confirm that today]. In that case, I'm wondering if we should actually change the default behaviour here (so as a 5.0 release actually change the default behaviour).

What are you guys thinking?

@Override
@Nullable
public Optional<Entry> fallbackValue(String key) {
String val = System.getProperty(key);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For testing purposes, it is better not to just simulate/mock the System.getProperty behaviour? I fear that if the tests are run concurrently that there may be clash in the runtime state of the properties as they are essentially static.

@andponlin
Copy link
Contributor Author

perhaps instead of a new fallback interface, we should change the behavior in general to prefer system/env variables over configuration files.
I'm wondering if we should actually change the default behaviour here (so as a 5.0 release actually change the default behaviour).

  • I agree that getting alignment with Spring on this would be really good as "out of the box" behaviour on the next major version.
  • I think the interfaces Rob has added appear to make sense to me and if somebody wanted to do something more bespoke they would be enabled to do so by using these interfaces.

BTW; will need to drop off now -- will reply to anything this evening (NZDT).

@SentryMan
Copy link
Collaborator

I think the interfaces Rob has added appear to make sense to me

they make sense to me as well, but if the only usecase at the moment is solved by spring env behavior, I'm not really seeing the point in adding it

@rob-bygrave
Copy link
Contributor

but if the only usecase at the moment is solved by ...

Yeah, I'm feeling that too. Leaning to making this a behaviour change without introducing the interface/plugin at least initially and seeing how that goes.

@rbygrave
Copy link
Contributor

rbygrave commented Dec 8, 2025

FYI: Spring Behaviour: System Properties overrides Environment Variables overrides ... property source values

[So Andrew, this was a touch different in that System Properties trumps Environment Variables ... is that an issue?]

I think we adjust this PR such that it matches Spring behaviour, and remove the interface/pluggable part at least initially and look at that as a 5.0 version with that behaviour change [and maybe release what's in master now as 4.3?]

@rob-bygrave
Copy link
Contributor

I think we should use #239

@rbygrave
Copy link
Contributor

rbygrave commented Dec 9, 2025

Replaced by #239

@rbygrave rbygrave closed this Dec 9, 2025
@rbygrave
Copy link
Contributor

rbygrave commented Dec 9, 2025

@andponlin Try version 5.0-RC1 ... which matches Spring behaviour and I think is what you are looking for (except noting that System properties trump Environment vars).

@andponlin
Copy link
Contributor Author

Hi @rbygrave ;

which matches Spring behaviour and I think is what you are looking for (except noting that System properties trump Environment vars).

It seems more logical that it would be the other way around with env-vars being precedence but for my situation specifically where I a using this it won't be a problem. Thanks both; I will try this out.

@andponlin
Copy link
Contributor Author

Tried it out; seems to be working well 👍

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