Added class SignalHandler
Singleton class Signal Handler is responsible for interacting with signal module: allow to have several handlers assigned to distinct signal. Added correct handling SIGINT signal. Closes-Bug: #1477102 Change-Id: I4bc4c98645e9afeb18635576d50a442fb7ce8425
This commit is contained in:
parent
3d9ae77c30
commit
e3983f23b5
@ -18,6 +18,7 @@
|
||||
"""Generic Node base class for all workers that run on hosts."""
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import copy
|
||||
import errno
|
||||
import io
|
||||
@ -32,6 +33,7 @@ import time
|
||||
import eventlet
|
||||
from eventlet import event
|
||||
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_service import eventlet_backdoor
|
||||
from oslo_service._i18n import _LE, _LI, _LW
|
||||
from oslo_service import _options
|
||||
@ -39,17 +41,6 @@ from oslo_service import systemd
|
||||
from oslo_service import threadgroup
|
||||
|
||||
|
||||
# Map all signal names to signal integer values and create a
|
||||
# reverse mapping (for easier + quick lookup).
|
||||
_ignore_signals = ('SIG_DFL', 'SIG_IGN')
|
||||
_signals_by_name = dict((name, getattr(signal, name))
|
||||
for name in dir(signal)
|
||||
if name.startswith("SIG")
|
||||
and name not in _ignore_signals)
|
||||
_signals_to_name = dict((sigval, name)
|
||||
for (name, sigval) in _signals_by_name.items())
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -59,10 +50,6 @@ def list_opts():
|
||||
_options.service_opts))]
|
||||
|
||||
|
||||
def _sighup_supported():
|
||||
return 'SIGHUP' in _signals_by_name
|
||||
|
||||
|
||||
def _is_daemon():
|
||||
# The process group for a foreground process will match the
|
||||
# process group of the controlling terminal. If those values do
|
||||
@ -84,19 +71,13 @@ def _is_daemon():
|
||||
|
||||
|
||||
def _is_sighup_and_daemon(signo):
|
||||
if not (_sighup_supported() and signo == signal.SIGHUP):
|
||||
if not (SignalHandler().is_sighup_supported and signo == signal.SIGHUP):
|
||||
# Avoid checking if we are a daemon, because the signal isn't
|
||||
# SIGHUP.
|
||||
return False
|
||||
return _is_daemon()
|
||||
|
||||
|
||||
def _set_signals_handler(handler):
|
||||
signal.signal(signal.SIGTERM, handler)
|
||||
if _sighup_supported():
|
||||
signal.signal(signal.SIGHUP, handler)
|
||||
|
||||
|
||||
def _check_service_base(service):
|
||||
if not isinstance(service, ServiceBase):
|
||||
raise TypeError("Service %(service)s must an instance of %(base)s!"
|
||||
@ -127,6 +108,56 @@ class ServiceBase(object):
|
||||
"""
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
with lockutils.lock('singleton_lock'):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(
|
||||
*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
@six.add_metaclass(Singleton)
|
||||
class SignalHandler(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SignalHandler, self).__init__(*args, **kwargs)
|
||||
# Map all signal names to signal integer values and create a
|
||||
# reverse mapping (for easier + quick lookup).
|
||||
self._ignore_signals = ('SIG_DFL', 'SIG_IGN')
|
||||
self._signals_by_name = dict((name, getattr(signal, name))
|
||||
for name in dir(signal)
|
||||
if name.startswith("SIG")
|
||||
and name not in self._ignore_signals)
|
||||
self.signals_to_name = dict(
|
||||
(sigval, name)
|
||||
for (name, sigval) in self._signals_by_name.items())
|
||||
self.is_sighup_supported = 'SIGHUP' in self._signals_by_name
|
||||
self._signal_handlers = collections.defaultdict(set)
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
for sig in self._signal_handlers:
|
||||
signal.signal(sig, signal.SIG_DFL)
|
||||
self._signal_handlers.clear()
|
||||
|
||||
def add_handler(self, signals, handler):
|
||||
if isinstance(signals, collections.Iterable):
|
||||
for sig in signals:
|
||||
self.add_handler(sig, handler)
|
||||
return
|
||||
sig = signals
|
||||
if sig == signal.SIGHUP and not self.is_sighup_supported:
|
||||
return
|
||||
self._signal_handlers[sig].add(handler)
|
||||
signal.signal(sig, self._handle_signals)
|
||||
|
||||
def _handle_signals(self, signo, frame):
|
||||
for handler in self._signal_handlers[signo]:
|
||||
handler(signo, frame)
|
||||
|
||||
|
||||
class Launcher(object):
|
||||
"""Launch one or more services and wait for them to complete."""
|
||||
|
||||
@ -203,12 +234,14 @@ class ServiceLauncher(Launcher):
|
||||
:raises SignalExit
|
||||
"""
|
||||
# Allow the process to be killed again and die from natural causes
|
||||
_set_signals_handler(signal.SIG_DFL)
|
||||
SignalHandler().clear()
|
||||
raise SignalExit(signo)
|
||||
|
||||
def handle_signal(self):
|
||||
"""Set self._handle_signal as a signal handler."""
|
||||
_set_signals_handler(self._handle_signal)
|
||||
SignalHandler().add_handler(
|
||||
(signal.SIGTERM, signal.SIGHUP, signal.SIGINT),
|
||||
self._handle_signal)
|
||||
|
||||
def _wait_for_exit_or_signal(self, ready_callback=None):
|
||||
status = None
|
||||
@ -223,7 +256,7 @@ class ServiceLauncher(Launcher):
|
||||
ready_callback()
|
||||
super(ServiceLauncher, self).wait()
|
||||
except SignalExit as exc:
|
||||
signame = _signals_to_name[exc.signo]
|
||||
signame = SignalHandler().signals_to_name[exc.signo]
|
||||
LOG.info(_LI('Caught %s, exiting'), signame)
|
||||
status = exc.code
|
||||
signo = exc.signo
|
||||
@ -240,6 +273,7 @@ class ServiceLauncher(Launcher):
|
||||
:returns: termination status
|
||||
"""
|
||||
systemd.notify_once()
|
||||
SignalHandler().clear()
|
||||
while True:
|
||||
self.handle_signal()
|
||||
status, signo = self._wait_for_exit_or_signal(ready_callback)
|
||||
@ -258,17 +292,6 @@ class ServiceWrapper(object):
|
||||
|
||||
class ProcessLauncher(object):
|
||||
"""Launch a service with a given number of workers."""
|
||||
_signal_handlers_set = set()
|
||||
|
||||
@classmethod
|
||||
def _handle_class_signals(cls, *args, **kwargs):
|
||||
"""Call all registered class handlers.
|
||||
|
||||
That is needed in case there are multiple ProcessLauncher
|
||||
instances in one process.
|
||||
"""
|
||||
for handler in cls._signal_handlers_set:
|
||||
handler(*args, **kwargs)
|
||||
|
||||
def __init__(self, conf, wait_interval=0.01):
|
||||
"""Constructor.
|
||||
@ -286,12 +309,14 @@ class ProcessLauncher(object):
|
||||
self.launcher = None
|
||||
rfd, self.writepipe = os.pipe()
|
||||
self.readpipe = eventlet.greenio.GreenPipe(rfd, 'r')
|
||||
self.signal_handler = SignalHandler()
|
||||
self.handle_signal()
|
||||
|
||||
def handle_signal(self):
|
||||
"""Add instance's signal handlers to class handlers."""
|
||||
self._signal_handlers_set.add(self._handle_signal)
|
||||
_set_signals_handler(self._handle_class_signals)
|
||||
self.signal_handler.add_handler((signal.SIGTERM, signal.SIGHUP),
|
||||
self._handle_signal)
|
||||
self.signal_handler.add_handler(signal.SIGINT, self._fast_exit)
|
||||
|
||||
def _handle_signal(self, signo, frame):
|
||||
"""Set signal handlers.
|
||||
@ -303,7 +328,11 @@ class ProcessLauncher(object):
|
||||
self.running = False
|
||||
|
||||
# Allow the process to be killed again and die from natural causes
|
||||
_set_signals_handler(signal.SIG_DFL)
|
||||
self.signal_handler.clear()
|
||||
|
||||
def _fast_exit(self, signo, frame):
|
||||
LOG.info(_LI('Caught SIGINT signal, instantaneous exiting'))
|
||||
os._exit(1)
|
||||
|
||||
def _pipe_watcher(self):
|
||||
# This will block until the write end is closed when the parent
|
||||
@ -321,17 +350,19 @@ class ProcessLauncher(object):
|
||||
# Setup child signal handlers differently
|
||||
|
||||
def _sigterm(*args):
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
SignalHandler().clear()
|
||||
self.launcher.stop()
|
||||
|
||||
def _sighup(*args):
|
||||
signal.signal(signal.SIGHUP, signal.SIG_DFL)
|
||||
SignalHandler().clear()
|
||||
raise SignalExit(signal.SIGHUP)
|
||||
|
||||
self.signal_handler.clear()
|
||||
|
||||
# Parent signals with SIGTERM when it wants us to go away.
|
||||
signal.signal(signal.SIGTERM, _sigterm)
|
||||
if _sighup_supported():
|
||||
signal.signal(signal.SIGHUP, _sighup)
|
||||
self.signal_handler.add_handler(signal.SIGTERM, _sigterm)
|
||||
self.signal_handler.add_handler(signal.SIGHUP, _sighup)
|
||||
self.signal_handler.add_handler(signal.SIGINT, self._fast_exit)
|
||||
|
||||
def _child_wait_for_exit_or_signal(self, launcher):
|
||||
status = 0
|
||||
@ -343,7 +374,7 @@ class ProcessLauncher(object):
|
||||
try:
|
||||
launcher.wait()
|
||||
except SignalExit as exc:
|
||||
signame = _signals_to_name[exc.signo]
|
||||
signame = self.signal_handler.signals_to_name[exc.signo]
|
||||
LOG.info(_LI('Child caught %s, exiting'), signame)
|
||||
status = exc.code
|
||||
signo = exc.signo
|
||||
@ -480,7 +511,7 @@ class ProcessLauncher(object):
|
||||
if not self.sigcaught:
|
||||
return
|
||||
|
||||
signame = _signals_to_name[self.sigcaught]
|
||||
signame = self.signal_handler.signals_to_name[self.sigcaught]
|
||||
LOG.info(_LI('Caught %s, stopping children'), signame)
|
||||
if not _is_sighup_and_daemon(self.sigcaught):
|
||||
break
|
||||
|
@ -370,28 +370,20 @@ class ProcessLauncherTest(base.ServiceBaseTestCase):
|
||||
mock_kill.mock_calls)
|
||||
mock_service_stop.assert_called_once_with()
|
||||
|
||||
@mock.patch(
|
||||
"oslo_service.service.ProcessLauncher._signal_handlers_set",
|
||||
new_callable=lambda: set())
|
||||
def test__signal_handlers_set(self, signal_handlers_set_mock):
|
||||
callables = set()
|
||||
l1 = service.ProcessLauncher(self.conf)
|
||||
callables.add(l1._handle_signal)
|
||||
self.assertEqual(1, len(service.ProcessLauncher._signal_handlers_set))
|
||||
l2 = service.ProcessLauncher(self.conf)
|
||||
callables.add(l2._handle_signal)
|
||||
self.assertEqual(2, len(service.ProcessLauncher._signal_handlers_set))
|
||||
self.assertEqual(callables,
|
||||
service.ProcessLauncher._signal_handlers_set)
|
||||
|
||||
@mock.patch(
|
||||
"oslo_service.service.ProcessLauncher._signal_handlers_set",
|
||||
new_callable=lambda: set())
|
||||
def test__handle_class_signals(self, signal_handlers_set_mock):
|
||||
signal_handlers_set_mock.update([mock.Mock(), mock.Mock()])
|
||||
service.ProcessLauncher._handle_class_signals()
|
||||
for m in service.ProcessLauncher._signal_handlers_set:
|
||||
m.assert_called_once_with()
|
||||
def test__handle_signals(self):
|
||||
signal_handler = service.SignalHandler()
|
||||
signal_handler.clear()
|
||||
self.assertEqual(0,
|
||||
len(signal_handler._signal_handlers[signal.SIGTERM]))
|
||||
call_1, call_2 = mock.Mock(), mock.Mock()
|
||||
signal_handler.add_handler(signal.SIGTERM, call_1)
|
||||
signal_handler.add_handler(signal.SIGTERM, call_2)
|
||||
self.assertEqual(2,
|
||||
len(signal_handler._signal_handlers[signal.SIGTERM]))
|
||||
signal_handler._handle_signals(signal.SIGTERM, 'test')
|
||||
for m in signal_handler._signal_handlers[signal.SIGTERM]:
|
||||
m.assert_called_once_with(signal.SIGTERM, 'test')
|
||||
signal_handler.clear()
|
||||
|
||||
@mock.patch("os.kill")
|
||||
@mock.patch("oslo_service.service.ProcessLauncher.stop")
|
||||
|
@ -7,6 +7,7 @@ eventlet>=0.17.4
|
||||
greenlet>=0.3.2
|
||||
monotonic>=0.1 # Apache-2.0
|
||||
oslo.utils>=1.9.0 # Apache-2.0
|
||||
oslo.concurrency>=2.3.0 # Apache-2.0
|
||||
oslo.config>=1.11.0 # Apache-2.0
|
||||
six>=1.9.0
|
||||
oslo.i18n>=1.5.0 # Apache-2.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user