Mutability Support

An in-memory config tree, once loaded, is immutable, even though the data in the underlying config sources can change over time. Your application can find out metadata about a loaded in-memory config and can track changes in config sources.

Overview

Even though in-memory config trees are immutable, the config system internally records which config sources it used to load each config tree and some metadata about the configuration. Your application can be aware of updates to the underlying config sources by:

  1. using the metadata the config system maintains,
  2. responding to changes when the config sources are updated, or
  3. using Suppliers of particular config values to obtain the always-current value for a key.

Using Config Metadata

Loading Time

The config system records when it loads each configuration into memory. Your application can retrieve it by invoking the timestamp method:

java.time.Instance loadTime = myConfig.timestamp();
Copied

on any config node.

Config Context

The config system maintains a Config.Context for each Config node. Your application can retrieve the context by invoking the Config.context() method and then use it for these operations:

Uses of Config.Context
MethodUsage
Instant timestamp()Returns the load time of the last loaded configuration that used the context.
Config last()Returns the most recently loaded configuration that used the context.
Config reload()Reloads the entire config tree from the current contents of the same config sources used to load the tree in which the current node resides.

Note that the config context describes or replaces a currently-loaded config tree. It by itself does not help your application decide when reloading the config might be useful.

Responding to Changes in Config Sources

Evolving API

This section describes the Config.changes() method. It is marked as deprecated because it returns an io.helidon.reactive.Flow.Publisher object. In a future Helidon release that requires Java 11 or later this method will be undeprecated and changed — or a similar method will be added — so that the return type is java.util.concurrent.Flow.Publisher instead.

Any code you write using the existing Config.changes() method might need to change at that time.

Although in-memory config trees do not change once loaded, applications can respond to changes in the underlying config sources by:

  1. setting up change detection for the config sources used to build a configuration, and
  2. registering a response to be run when a source changes.

Your code’s response can react to the changes in whatever way makes sense for your application.

The following sections describe these steps in detail.

Setting up Config Source Change Detection

When the application creates a config source, it can set up change detection for that source. This is called polling in the Helidon API but specific change detection algorithms might not use actual polling. You choose a specific PollingStrategy for each config source you want to monitor. See the section on polling strategies in the config extensions doc page for more information.

The config system provides some built-in polling strategies, exposed as these methods on the PollingStrategies class:

  • regular(Duration interval) - a general-purpose scheduled polling strategy with a specified, constant polling interval.

  • watch(Path watchedPath) - a filesystem-specific strategy to watch specified path. You can use this strategy with the file and classpath built-in config sources.

  • nop() - a no-op strategy

This example builds a Config object from three sources, each set up with a different polling strategy:

Build a Config with a different PollingStrategy for each config source
Config config = Config.create(
        ConfigSources.file("conf/dev.properties")
                .pollingStrategy(PollingStrategies.regular(Duration.ofSeconds(2))) 
                .optional(),
        ConfigSources.file("conf/config.properties")
                .changeWatcher(FileSystemWatcher.create())                         
                .optional(),
        ConfigSources.classpath("application.properties")
                .pollingStrategy(PollingStrategies::nop));                         
Copied
  • Optional file source conf/dev.properties will be checked for changes every 2 seconds.
  • Optional file source conf/config.properties will be watched by the Java WatchService for changes on filesystem.
  • The classpath resource application.properties will not be checked for changes. PollingStrategies.nop() polling strategy is default.

The polling strategies internally inform the config system when they detect changes in the monitored config sources (except that the nop strategy does nothing).

Registering a Config Change Response

To know when config sources have changed, your application must register its interest on the Config node of interest. The config system will then notify your application of any change within the subtree rooted at that node. In particular, if you register on the root node, then the config system notifies your code of changes anywhere in the config tree.

You can register in either of two ways:

  1. register an action to be run upon each change, or
  2. subscribe to a Flow.Publisher that notifies of changes.

Registering Actions

A simple approach is for your application to register a function that should run when any change occurs.

Subscribe on greeting property changes via onChange method
config.get("greeting")                                                         
        .onChange(changedNode -> {                                             
            System.out.println("Node " + changedNode.key() + " has changed!");
        });
Copied
  • Navigate to the Config node on which you want to register.
  • Invoke the onChange method, passing a consumer (Consumer<Config>). The config system invokes that consumer each time the subtree rooted at the greeting node changes. The changedNode is a new instance of Config representing the updated subtree rooted at greeting.

Subscribing to Events

The config system also supports the flow publisher/subscriber model for applications that need more control over the pace at which the config system delivers config change events.

Each Config instance exposes the Config.changes() method which returns a Flow.Publisher<Config>. Your application can invoke this method, then invoke subscribe on the returned Flow.Publisher, passing your own Flow.Subscriber implementation. The config system will invoke your subscriber’s methods as appropriate, most notably calling onNext whenever it detects a change in one of the underlying config sources for the config node of interest.

Mote that your subscriber will be notified when a change occurs anywhere in the subtree represented by the Config node.

Subscribe on greeting property changes
config.get("greeting")                                                             
        .changes()                                                                 
        .subscribe(new Flow.Subscriber<>() {                                       
            Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {              
                this.subscription = subscription;
                subscription.request(1);
            }

            @Override
            public void onNext(Config changedNode) {                               
                System.out.println("Node " + changedNode.key() + " has changed!");
                subscription.request(1);
            }

            @Override
            public void onError(Throwable throwable) {                             
            }

            @Override
            public void onComplete() {                                             
            }
        });
Copied
  • Navigate to the Config node on which you want to register.
  • Invoke changes to get the Flow.Publisher of changes to the subtree rooted at the Config node.
  • Subscribe to the publisher passing a custom Flow.Subscriber<Config> implementation.
  • Request the first event delivery in onSubscribe method.
  • The config system invokes onNext each time the subtree rooted at the greeting node changes. The changedNode is a new instance of Config representing the updated subtree rooted at greeting, regardless of where in the subtree the change actually occurred. Remember to request the next event delivery in onNext.
  • The config system does not currently invoke onError.
  • The config system invokes onComplete if all config sources indicate there will be no other change event.

Note

Your application does not need to subscribe to the new Config instance passed to your onNext method. The original subscription remains in force for changes to the "new" instance.

Accessing Always-current Values

Some applications do not need to respond to changes as they happen. Instead it’s sufficient that they simply have access to the current value for a particular key in the configuration.

Each asXXX method on the Config class has a companion asXXXSupplier method. These supplier methods return Supplier<XXX>, and when your application invokes the supplier’s get method the config system returns the then-current value as stored in the config source.

Access greeting property as Supplier
// Construct a Config with the appropriate PollingStrategy on each config source.

Supplier<String> greetingSupplier = config.get("greeting")                     
        .asString().supplier();                                                   

System.out.println("Always actual greeting value: " + greetingSupplier.get()); 
Copied
  • Navigate to the Config node for which you want access to the always-current value.
  • Retrieve and store the returned supplier for later use.
  • Invoke the supplier’s get() method to retrieve the current value of the node.

Important

Supplier support requires that you create the Config object from config sources that have proper polling strategies set up.