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

Way to Register Change Listener? #64

Closed
MrThreepwood opened this issue Feb 10, 2021 · 10 comments
Closed

Way to Register Change Listener? #64

MrThreepwood opened this issue Feb 10, 2021 · 10 comments

Comments

@MrThreepwood
Copy link

Is there a way to call a callback when a value changes? We can setup watchFile to look for changes which is really nifty, but is there any way to be alerted when a property has changed, or even just be alerted when the file is changed?

Since the values in a property file are often used for longer lived classes we often need to take action when a property has changed. Specifically in my case, our DB usernames and passwords rotate, and i need to update my connection pool whenever this happens.

@MrThreepwood MrThreepwood changed the title Way to Call callback on Change? Way to Register Change Listener? Feb 11, 2021
@uchuhimo
Copy link
Owner

@MrThreepwood You can use the following API to subscribe the update event of any item:

object Server : ConfigSpec() {
    val host by optional("0.0.0.0")
}

val handler = Server.host.onSet { value -> println("the host has changed to $value") }

// cancel the subscription
handler.cancel()

I have added the API description to README.

@dieterdeagle
Copy link

Hope its ok to misuse this issue for a follow-up question:

@uchuhimo When using onSet to trigger some kind of "re-read" of the changed config value (reading from the config not from the provided value in onSet), a com.uchuhimo.konf.UnsetValueException is raised.

It seems that this happens because the onSet is triggered before the actual setState is performed (in BaseConfig.kt):

item.notifySet(value)
lock.write {
    setState(item, ValueState.Value(value))
}

My use case is that I want to reconfigure my application by reading from the current config after the config was changed. Is there some other way to do this? Or is there no special reason for having the notifySet prior to the setState and we could maybe even swap those two?

Thanks a lot and best regards!

@uchuhimo
Copy link
Owner

uchuhimo commented Apr 1, 2021

@dieterdeagle If notifySet is invoked after setState, you cannot access the old value of the item anymore. You can check whether an item exists in config by item in config to prevent com.uchuhimo.konf.UnsetValueException.

@dieterdeagle
Copy link

@uchuhimo But why would one want to access the old value of the item? Is that a real use case? And isn't that the case anyway because the Item is 'unset' - so you can't read the old nor the new value?

I probably didn't explain well enough what I was trying to achieve, so I'll give it another try (or maybe I am misunderstanding a basic concept?):

So let's say we have some kind of Spec called ServerSpec which we use in a config that is read from a watched file:

object ServerSpec : ConfigSpec("server") {
    val someImportantProperty by required<String>()
}

val config = Config { addSpec(ServerSpec) }.from.yaml.watchFile("myConfig.yml")

After that, we do the initial expensive setup of our application based on this config:

doExpensiveStuffToSetupMyApplication(config)

where doExpensiveStuffToSetupMyApplication looks something like this:

fun doExpensiveStuffToSetupMyApplication(config: Config) {
    val myImportantValue = config[ServerSpec.someImportantProperty]
  
    // do expensive stuff with myImportantValue (omitted here)
    // ...
}

After this initial setup, we would like to re-trigger this expensive setup using the then new value(s) whenever (but only then) there was a change to the item. So my idea was, to register a listener that calls doExpensiveStuffToSetupMyApplication when a change occurs:

ServerSpec.someImportantProperty.onSet {
    doExpensiveStuffToSetupMyApplication(config)
}

My expectation was, that when a listener is informed about a change to a property, he can read the new value from the config. But apparently, the value is in an unset state when the listener is called - which leads to the exception.
Hence my assumption, that changing the order (inform listener after actually doing the change) would 'fix' (if this behaviour isn't a case of 'works as intended') this at least for me slightly surprising behaviour.

If the current implementation is in fact behaving as intended: is there a 'correct' way to achieve what I tried to explain above?

Thanks a lot for your time! :)

@uchuhimo
Copy link
Owner

uchuhimo commented Apr 2, 2021

@dieterdeagle onSet is not a good solution for your case. I would like to solve it by adding a callback to watchFile:

Config { addSpec(ServerSpec) }.from.yaml.watchFile("myConfig.yml") { config ->
    doExpensiveStuffToSetupMyApplication(config)
}

so that the setup will be triggered when file is reloaded.

Any suggestion?

@dieterdeagle
Copy link

dieterdeagle commented Apr 5, 2021

@uchuhimo That would definitely work and would be much better than triggering the setup on individual property updates.
API-wise/Conceptionally, I think an even clearer approach would be, to be able listen to changes of the config itself and not just the underlying individual files (which is more or less a technical detail). In general, I guess, one want be usually interested in re-triggering a config-based setup when the config itself changes - no matter where those changes came from. What do you think? Does this make sense and is somehow feasible?

@uchuhimo
Copy link
Owner

uchuhimo commented Apr 5, 2021

@dieterdeagle If a config-based setup is re-triggered once the config changes, a config file with 10 items will trigger 10 expensive setups.
I will try to provide both source-based listener and config-based listener.

@dieterdeagle
Copy link

@uchuhimo That's a good point, didn't think about that.
Thanks a lot! :)

@uchuhimo
Copy link
Owner

uchuhimo commented Apr 8, 2021

@dieterdeagle I have released v1.1.1, which contains all the listeners mentioned above. Check release notes (https://github.com/uchuhimo/konf/releases/tag/v1.1.1) and README (https://github.com/uchuhimo/konf) for the new APIs. For example, you can use the following API for your use case:

Config { addSpec(ServerSpec) }.from.yaml.watchFile("myConfig.yml") { config, source ->
    doExpensiveStuffToSetupMyApplication(config)
}

or

val config = Config { addSpec(ServerSpec) }.from.yaml.watchFile("myConfig.yml")
config.afterLoad { source -> doExpensiveStuffToSetupMyApplication(config) }

@dieterdeagle
Copy link

@uchuhimo Works like a charm!

Again: Thanks a lot! :)

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

No branches or pull requests

3 participants