Skip to content

Commit

Permalink
Add support for dynamic configuration updates
Browse files Browse the repository at this point in the history
Long-running, service-oriented, applications should be able to have their
configurations updated without requiring restarts.  These changes add methods
that support periodic checks of previously loaded configuration files for
changes and updates configuration-based traitlets of registered Configurable
instances.
  • Loading branch information
kevin-bates committed May 2, 2019
1 parent 3eea7fc commit 563b6be
Showing 1 changed file with 60 additions and 2 deletions.
62 changes: 60 additions & 2 deletions traitlets/config/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pprint
import re
import sys
import time

from traitlets.config.configurable import Configurable, SingletonConfigurable
from traitlets.config.loader import (
Expand Down Expand Up @@ -315,6 +316,9 @@ def __init__(self, **kwargs):
else:
self.classes.insert(0, self.__class__)

self.last_config_update = int(time.time())
self.dynamic_configurables = {}

@observe('config')
@observe_compat
def _config_changed(self, change):
Expand All @@ -329,7 +333,6 @@ def initialize(self, argv=None):
"""
self.parse_command_line(argv)


def start(self):
"""Start the app mainloop.
Expand Down Expand Up @@ -763,7 +766,8 @@ def load_config_file(self, filename, path=None):
raise_config_file_errors=self.raise_config_file_errors,
):
new_config.merge(config)
self._loaded_config_files.append(filename)
if filename not in self._loaded_config_files: # only add if not there (support reloads)
self._loaded_config_files.append(filename)
# add self.cli_config to preserve CLI config priority
new_config.merge(self.cli_config)
self.update_config(new_config)
Expand Down Expand Up @@ -820,6 +824,60 @@ def exit(self, exit_status=0):
self.log.debug("Exiting application: %s" % self.name)
sys.exit(exit_status)

def _config_files_updated(self):
"""
Checks the currently loaded config file modification times to see if any are
more recent than the last update. If newer files are detected, True is returned.
:return: bool
"""
updated = False
for file in self._loaded_config_files:
mod_time = int(os.path.getmtime(file))
if mod_time > self.last_config_update:
self.log.debug("Config file was updated: {}!".format(file))
self.last_config_update = mod_time
updated = True
# Rather than break here, exhaust all files so last_config_update is the latest.
return updated

def update_dynamic_configurables(self):
"""
Called periodically, this method checks if configuration file updates have occurred. If
updates where detected (last mod time changed), reload the configuration files and update
the list of configurables participating in dynamic updates.
:return: True if files were updated
"""
updated = False
configs = []
if self._config_files_updated():
# If files were updated, reload the config files into self.config, then
# update the config of each configurable from the newly loaded values.
# Note: We must be explicit when calling load_config_file() so as to not conflict
# with child class implementations (that are not overrides, e.g., JupyterApp).
for file in self._loaded_config_files:
Application.load_config_file(self, file)

for config_name, configurable in self.dynamic_configurables.items():
configurable.update_config(self.config)
configs.append(config_name)

updated = True
self.log.info("Configuration file changes detected. Instances for the following "
"configurables have been updated: {}".format(configs))
return updated

def add_dynamic_configurable(self, config_name, configurable):
"""
Adds the configurable instance associated with the given name to the list of Configurables
that can have their configurations updated when configuration file updates are detected.
:param config_name: the name of the config within this application
:param configurable: the configurable instance corresponding to that config
"""
if not isinstance(configurable, Configurable):
raise RuntimeError("'{}' is not a subclass of Configurable!".format(configurable))

self.dynamic_configurables[config_name] = configurable

@classmethod
def launch_instance(cls, argv=None, **kwargs):
"""Launch a global instance of this Application
Expand Down

0 comments on commit 563b6be

Please sign in to comment.