Reactive Triggers¶
Coupling Flags with Triggers¶
In general, it is best to be explicit about setting or clearing a flag. This makes the code more maintainable and easier to follow and reason about. However, rarely, due to the fact that handlers for a given flag are independent and thus there are no guarantees about the order in which they may execute, it is sometimes necessary to enforce that two flags must be set at the same time or that one must be cleared if the other is set.
As an example of when this might be necessary, consider a charm which provides two config values, one that determines the location from which resources should be fetched, with a default location provided by the charm, and another which indicates that a particular feature be installed and enabled. If the charm is deployed and fetches all of the resources, it might set a flag that indicates that all resources are available and any installation can proceed. However, if both resource location and feature flag config options are changed at the same time, the handlers might be invoked in an order that causes the feature installation to happen before the resource change has been observed, leading to the feature using the wrong resource. This problem is particularly intractable if the layer managing the resource location and readiness options is different than the layer managing the feature option, such as with the apt layer.
Triggers provide a mechanism for a flag to indicate that when a particular flag is set, another specific flag should be either set or cleared. To use a trigger, you simply have to register it, which can be done from inside a handler, or at the top level of your handlers file:
from charms.reactive.flags import register_trigger
from charms.reactive.flags import set_flag
from charms.reactive.decorators import when
register_trigger(when='flag_a',
set_flag='flag_b')
@when('flag_b')
def handler():
do_something()
register_trigger(when='flag_a',
clear_flag='flag_c')
set_flag('flag_c')
When a trigger is registered, then as soon as the flag given by when
is
set, the other flag is set or cleared at the same time. Thus, there is no
chance that another handler will run in between.
Keep in mind that since triggers are implicit, they should be used sparingly. Most use cases can be better modeled by explicitly setting and clearing flags.
Example uses of triggers¶
Remove flags immediately when config changes
In the apt
layer, the install_sources
config option specifies which
repositories and ppa’s to use for installing a package, so these need to be
added before installing any package. This is easy to do with flags: you create
a handler that adds the sources and then sets the flag
apt.sources_configured
. The handler that installs the packages reacts to
that flag with @when('apt.sources_configured')
. This works perfectly the
first time but what happens if the install_sources
config option gets
changed after they are first configured? Then the apt.sources_configured
flag needs to be cleared immediately before any new packages are installed.
This is where triggers come in: You create a trigger that unsets the
apt.sources_configured flag when the install_sources config changes.
register_trigger(when='config.changed.install_sources',
clear_flag='apt.sources_configured')
@when_not('apt.sources_configured')
def sources_handler():
configure_sources()
set_state('apt.sources_configured')
@when_all('apt.needs_update',
'apt.sources_configured')
def update():
charms.apt.update()
clear_flag('apt.sources_configured')
@when('apt.queued_installs')
@when_not('apt.needs_update')
def install_queued():
charms.apt.install_queued()
clear_flag('apt.queued_installs')
@when_not('apt.queued_installs')
def ensure_package_status():
charms.apt.ensure_package_status()