Offer mutate_config_files

This patch adds a 'restart_method' parameter to Launcher. When set to
'reload' (the default), behavior is as today. If a service chooses
'mutate' then when a SIGHUP is received, mutate_config_files is called
instead of reload_config_files. This means the 'mutable' flag on
options will be respected.

Change-Id: Icec3e664f3fe72614e373b2938e8dee53cf8bc5e
Depends-On: I9bb2ff39dc1ed0a63bd7e7635704b34c53d32d79
This commit is contained in:
Alexis Lee 2016-02-17 13:50:03 +00:00
parent 1393c85f32
commit 8f6383481b
2 changed files with 89 additions and 11 deletions

View File

@ -43,6 +43,8 @@ from oslo_service import threadgroup
LOG = logging.getLogger(__name__)
_LAUNCHER_RESTART_METHODS = ['reload', 'mutate']
def list_opts():
"""Entry point for oslo-config-generator."""
@ -182,9 +184,12 @@ class SignalHandler(object):
class Launcher(object):
"""Launch one or more services and wait for them to complete."""
def __init__(self, conf):
def __init__(self, conf, restart_method='reload'):
"""Initialize the service launcher.
:param restart_method: If 'reload', calls reload_config_files on
SIGHUP. If 'mutate', calls mutate_config_files on SIGHUP. Other
values produce a ValueError.
:returns: None
"""
@ -193,6 +198,9 @@ class Launcher(object):
self.services = Services()
self.backdoor_port = (
eventlet_backdoor.initialize_if_enabled(self.conf))
self.restart_method = restart_method
if restart_method not in _LAUNCHER_RESTART_METHODS:
raise ValueError(_("Invalid restart_method: %s") % restart_method)
def launch_service(self, service, workers=1):
"""Load and start the given service.
@ -230,10 +238,14 @@ class Launcher(object):
def restart(self):
"""Reload config files and restart service.
:returns: None
:returns: The return value from reload_config_files or
mutate_config_files, according to the restart_method.
"""
self.conf.reload_config_files()
if self.restart_method == 'reload':
self.conf.reload_config_files()
elif self.restart_method == 'mutate':
self.conf.mutate_config_files()
self.services.restart()
@ -245,12 +257,14 @@ class SignalExit(SystemExit):
class ServiceLauncher(Launcher):
"""Runs one or more service in a parent process."""
def __init__(self, conf):
def __init__(self, conf, restart_method='reload'):
"""Constructor.
:param conf: an instance of ConfigOpts
:param restart_method: passed to super
"""
super(ServiceLauncher, self).__init__(conf)
super(ServiceLauncher, self).__init__(
conf, restart_method=restart_method)
self.signal_handler = SignalHandler()
def _graceful_shutdown(self, *args):
@ -331,12 +345,15 @@ class ServiceWrapper(object):
class ProcessLauncher(object):
"""Launch a service with a given number of workers."""
def __init__(self, conf, wait_interval=0.01):
def __init__(self, conf, wait_interval=0.01, restart_method='reload'):
"""Constructor.
:param conf: an instance of ConfigOpts
:param wait_interval: The interval to sleep for between checks
of child process exit.
:param restart_method: If 'reload', calls reload_config_files on
SIGHUP. If 'mutate', calls mutate_config_files on SIGHUP. Other
values produce a ValueError.
"""
self.conf = conf
conf.register_opts(_options.service_opts)
@ -349,6 +366,9 @@ class ProcessLauncher(object):
self.readpipe = eventlet.greenio.GreenPipe(rfd, 'r')
self.signal_handler = SignalHandler()
self.handle_signal()
self.restart_method = restart_method
if restart_method not in _LAUNCHER_RESTART_METHODS:
raise ValueError(_("Invalid restart_method: %s") % restart_method)
def handle_signal(self):
"""Add instance's signal handlers to class handlers."""
@ -445,7 +465,7 @@ class ProcessLauncher(object):
# Reseed random number generator
random.seed()
launcher = Launcher(self.conf)
launcher = Launcher(self.conf, restart_method=self.restart_method)
launcher.launch_service(service)
return launcher
@ -560,7 +580,10 @@ class ProcessLauncher(object):
if not _is_sighup_and_daemon(self.sigcaught):
break
self.conf.reload_config_files()
if self.restart_method == 'reload':
self.conf.reload_config_files()
elif self.restart_method == 'mutate':
self.conf.mutate_config_files()
for service in set(
[wrap.service for wrap in self.children.values()]):
service.reset()
@ -690,13 +713,16 @@ class Services(object):
done.wait()
def launch(conf, service, workers=1):
def launch(conf, service, workers=1, restart_method='reload'):
"""Launch a service with a given number of workers.
:param conf: an instance of ConfigOpts
:param service: a service to launch, must be an instance of
:class:`oslo_service.service.ServiceBase`
:param workers: a number of processes in which a service will be running
:param restart_method: Passed to the constructed launcher. If 'reload', the
launcher will call reload_config_files on SIGHUP. If 'mutate', it will
call mutate_config_files on SIGHUP. Other values produce a ValueError.
:returns: instance of a launcher that was used to launch the service
"""
@ -704,9 +730,9 @@ def launch(conf, service, workers=1):
raise ValueError(_("Number of workers should be positive!"))
if workers is None or workers == 1:
launcher = ServiceLauncher(conf)
launcher = ServiceLauncher(conf, restart_method=restart_method)
else:
launcher = ProcessLauncher(conf)
launcher = ProcessLauncher(conf, restart_method=restart_method)
launcher.launch_service(service, workers=workers)
return launcher

View File

@ -284,6 +284,58 @@ class ServiceRestartTest(ServiceTestBase):
self.assertTrue(os.WIFEXITED(status))
self.assertEqual(os.WEXITSTATUS(status), 0)
def test_mutate_hook_service_launcher(self):
"""Test mutate_config_files is called by ServiceLauncher on SIGHUP.
Not using _spawn_service because ServiceLauncher doesn't fork and it's
simplest to stay all in one process.
"""
mutate = multiprocessing.Event()
self.conf.register_mutate_hook(lambda c, f: mutate.set())
launcher = service.launch(
self.conf, ServiceWithTimer(), restart_method='mutate')
self.assertFalse(mutate.is_set(), "Hook was called too early")
launcher.restart()
self.assertTrue(mutate.is_set(), "Hook wasn't called")
def test_mutate_hook_process_launcher(self):
"""Test mutate_config_files is called by ProcessLauncher on SIGHUP.
Forks happen in _spawn_service and ProcessLauncher. So we get three
tiers of processes, the top tier being the test process. self.pid
refers to the middle tier, which represents our application. Both
service_maker and launcher_maker execute in the middle tier. The bottom
tier is the workers.
The behavior we want is that when the application (middle tier)
receives a SIGHUP, it catches that, calls mutate_config_files and
relaunches all the workers. This causes them to inherit the mutated
config.
"""
mutate = multiprocessing.Event()
ready = multiprocessing.Event()
def service_maker():
self.conf.register_mutate_hook(lambda c, f: mutate.set())
return ServiceWithTimer(ready)
def launcher_maker():
return service.ProcessLauncher(self.conf, restart_method='mutate')
self.pid = self._spawn_service(1, service_maker, launcher_maker)
timeout = 5
ready.wait(timeout)
self.assertTrue(ready.is_set(), 'Service never became ready')
ready.clear()
self.assertFalse(mutate.is_set(), "Hook was called too early")
os.kill(self.pid, signal.SIGHUP)
ready.wait(timeout)
self.assertTrue(ready.is_set(), 'Service never back after SIGHUP')
self.assertTrue(mutate.is_set(), "Hook wasn't called")
class _Service(service.Service):
def __init__(self):