f88efdb4df
In order to modernize swift's statsd configuration we're working to separate it from logging. This change is a pre-requisite for the Related-Change in order to simplfy the stdlib base logger instance wrapping in a single extended SwiftLogAdapter (previously LogAdapter) which supports all the features swift's servers/daemons need from our logger instance interface. Related-Change-Id: I44694b92264066ca427bb96456d6f944e09b31c0 Change-Id: I8988c0add6bb4a65cc8be38f0bf527f141aac48a
491 lines
20 KiB
Python
491 lines
20 KiB
Python
# Copyright (c) 2010-2012 OpenStack Foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import os
|
|
import six
|
|
import time
|
|
import unittest
|
|
from getpass import getuser
|
|
import logging
|
|
from test.unit import tmpfile, with_tempdir, ConfigAssertMixin
|
|
import mock
|
|
import signal
|
|
from contextlib import contextmanager
|
|
import itertools
|
|
from collections import defaultdict
|
|
import errno
|
|
from textwrap import dedent
|
|
|
|
from swift.common import daemon, utils
|
|
from test.debug_logger import debug_logger
|
|
|
|
|
|
class MyDaemon(daemon.Daemon):
|
|
WORKERS_HEALTHCHECK_INTERVAL = 0
|
|
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
self.logger = debug_logger('my-daemon')
|
|
MyDaemon.forever_called = False
|
|
MyDaemon.once_called = False
|
|
|
|
def run_forever(self):
|
|
MyDaemon.forever_called = True
|
|
|
|
def run_once(self):
|
|
MyDaemon.once_called = True
|
|
|
|
def run_raise(self):
|
|
raise OSError
|
|
|
|
def run_quit(self):
|
|
raise KeyboardInterrupt
|
|
|
|
|
|
class TestDaemon(unittest.TestCase):
|
|
|
|
def test_create(self):
|
|
d = daemon.Daemon({})
|
|
self.assertEqual(d.conf, {})
|
|
self.assertIsInstance(d.logger, utils.logs.SwiftLogAdapter)
|
|
|
|
def test_stubs(self):
|
|
d = daemon.Daemon({})
|
|
self.assertRaises(NotImplementedError, d.run_once)
|
|
self.assertRaises(NotImplementedError, d.run_forever)
|
|
|
|
|
|
class MyWorkerDaemon(MyDaemon):
|
|
|
|
def __init__(self, *a, **kw):
|
|
super(MyWorkerDaemon, self).__init__(*a, **kw)
|
|
MyWorkerDaemon.post_multiprocess_run_called = False
|
|
|
|
def get_worker_args(self, once=False, **kwargs):
|
|
return [kwargs for i in range(int(self.conf.get('workers', 0)))]
|
|
|
|
def is_healthy(self):
|
|
try:
|
|
return getattr(self, 'health_side_effects', []).pop(0)
|
|
except IndexError:
|
|
return True
|
|
|
|
def post_multiprocess_run(self):
|
|
MyWorkerDaemon.post_multiprocess_run_called = True
|
|
|
|
|
|
class TestWorkerDaemon(unittest.TestCase):
|
|
|
|
def test_stubs(self):
|
|
d = daemon.Daemon({})
|
|
self.assertRaises(NotImplementedError, d.run_once)
|
|
self.assertRaises(NotImplementedError, d.run_forever)
|
|
self.assertEqual([], d.get_worker_args())
|
|
self.assertEqual(True, d.is_healthy())
|
|
|
|
def test_my_worker_daemon(self):
|
|
d = MyWorkerDaemon({})
|
|
self.assertEqual([], d.get_worker_args())
|
|
self.assertTrue(d.is_healthy())
|
|
d = MyWorkerDaemon({'workers': '3'})
|
|
self.assertEqual([{'key': 'val'}] * 3, d.get_worker_args(key='val'))
|
|
d.health_side_effects = [True, False]
|
|
self.assertTrue(d.is_healthy())
|
|
self.assertFalse(d.is_healthy())
|
|
self.assertTrue(d.is_healthy())
|
|
|
|
|
|
class TestRunDaemon(unittest.TestCase, ConfigAssertMixin):
|
|
|
|
def setUp(self):
|
|
for patcher in [
|
|
mock.patch.object(utils, 'HASH_PATH_PREFIX', b'startcap'),
|
|
mock.patch.object(utils, 'HASH_PATH_SUFFIX', b'endcap'),
|
|
mock.patch.object(utils, 'drop_privileges', lambda *args: None),
|
|
mock.patch.object(utils, 'capture_stdio', lambda *args: None),
|
|
]:
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def test_run(self):
|
|
d = MyDaemon({})
|
|
self.assertFalse(MyDaemon.forever_called)
|
|
self.assertFalse(MyDaemon.once_called)
|
|
# test default
|
|
d.run()
|
|
self.assertEqual(d.forever_called, True)
|
|
# test once
|
|
d.run(once=True)
|
|
self.assertEqual(d.once_called, True)
|
|
|
|
def test_signal(self):
|
|
d = MyDaemon({})
|
|
with mock.patch('swift.common.daemon.signal') as mock_signal:
|
|
mock_signal.SIGTERM = signal.SIGTERM
|
|
daemon.DaemonStrategy(d, d.logger).run()
|
|
signal_args, kwargs = mock_signal.signal.call_args
|
|
sig, func = signal_args
|
|
self.assertEqual(sig, signal.SIGTERM)
|
|
with mock.patch('swift.common.daemon.os') as mock_os:
|
|
func()
|
|
self.assertEqual(mock_os.method_calls, [
|
|
mock.call.getpid(),
|
|
mock.call.killpg(0, signal.SIGTERM),
|
|
# hard exit because bare except handlers can trap SystemExit
|
|
mock.call._exit(0)
|
|
])
|
|
|
|
def test_run_daemon(self):
|
|
logging.logThreads = 1 # reset to default
|
|
sample_conf = "[my-daemon]\nuser = %s\n" % getuser()
|
|
with tmpfile(sample_conf) as conf_file, \
|
|
mock.patch('swift.common.utils.eventlet') as _utils_evt, \
|
|
mock.patch('eventlet.hubs.use_hub') as mock_use_hub, \
|
|
mock.patch('eventlet.debug') as _debug_evt:
|
|
with mock.patch.dict('os.environ', {'TZ': ''}), \
|
|
mock.patch('time.tzset') as mock_tzset:
|
|
daemon.run_daemon(MyDaemon, conf_file)
|
|
self.assertTrue(MyDaemon.forever_called)
|
|
self.assertEqual(os.environ['TZ'], 'UTC+0')
|
|
self.assertEqual(mock_tzset.mock_calls, [mock.call()])
|
|
self.assertEqual(mock_use_hub.mock_calls,
|
|
[mock.call(utils.get_hub())])
|
|
daemon.run_daemon(MyDaemon, conf_file, once=True)
|
|
_utils_evt.patcher.monkey_patch.assert_called_with(all=False,
|
|
socket=True,
|
|
select=True,
|
|
thread=True)
|
|
self.assertEqual(0, logging.logThreads) # fixed in monkey_patch
|
|
_debug_evt.hub_exceptions.assert_called_with(False)
|
|
self.assertEqual(MyDaemon.once_called, True)
|
|
|
|
# test raise in daemon code
|
|
with mock.patch.object(MyDaemon, 'run_once', MyDaemon.run_raise):
|
|
self.assertRaises(OSError, daemon.run_daemon, MyDaemon,
|
|
conf_file, once=True)
|
|
|
|
# test user quit
|
|
sio = six.StringIO()
|
|
logger = logging.getLogger('server')
|
|
logger.addHandler(logging.StreamHandler(sio))
|
|
logger = utils.get_logger(None, 'server', log_route='server')
|
|
with mock.patch.object(MyDaemon, 'run_forever', MyDaemon.run_quit):
|
|
daemon.run_daemon(MyDaemon, conf_file, logger=logger)
|
|
self.assertTrue('user quit' in sio.getvalue().lower())
|
|
|
|
# test missing section
|
|
sample_conf = "[default]\nuser = %s\n" % getuser()
|
|
with tmpfile(sample_conf) as conf_file:
|
|
self.assertRaisesRegex(SystemExit,
|
|
'Unable to find my-daemon '
|
|
'config section in.*',
|
|
daemon.run_daemon, MyDaemon,
|
|
conf_file, once=True)
|
|
|
|
def test_run_daemon_diff_tz(self):
|
|
old_tz = os.environ.get('TZ', '')
|
|
try:
|
|
os.environ['TZ'] = 'EST+05EDT,M4.1.0,M10.5.0'
|
|
time.tzset()
|
|
self.assertEqual((1970, 1, 1, 0, 0, 0), time.gmtime(0)[:6])
|
|
self.assertEqual((1969, 12, 31, 19, 0, 0), time.localtime(0)[:6])
|
|
self.assertEqual(18000, time.timezone)
|
|
|
|
sample_conf = "[my-daemon]\nuser = %s\n" % getuser()
|
|
with tmpfile(sample_conf) as conf_file, \
|
|
mock.patch('swift.common.utils.eventlet'), \
|
|
mock.patch('eventlet.hubs.use_hub'), \
|
|
mock.patch('eventlet.debug'):
|
|
daemon.run_daemon(MyDaemon, conf_file)
|
|
self.assertFalse(MyDaemon.once_called)
|
|
self.assertTrue(MyDaemon.forever_called)
|
|
|
|
self.assertEqual((1970, 1, 1, 0, 0, 0), time.gmtime(0)[:6])
|
|
self.assertEqual((1970, 1, 1, 0, 0, 0), time.localtime(0)[:6])
|
|
self.assertEqual(0, time.timezone)
|
|
finally:
|
|
os.environ['TZ'] = old_tz
|
|
time.tzset()
|
|
|
|
@with_tempdir
|
|
def test_run_deamon_from_conf_file(self, tempdir):
|
|
conf_path = os.path.join(tempdir, 'test-daemon.conf')
|
|
conf_body = """
|
|
[DEFAULT]
|
|
conn_timeout = 5
|
|
client_timeout = 1
|
|
[my-daemon]
|
|
CONN_timeout = 10
|
|
client_timeout = 2
|
|
"""
|
|
contents = dedent(conf_body)
|
|
with open(conf_path, 'w') as f:
|
|
f.write(contents)
|
|
with mock.patch('swift.common.utils.eventlet'), \
|
|
mock.patch('eventlet.hubs.use_hub'), \
|
|
mock.patch('eventlet.debug'):
|
|
d = daemon.run_daemon(MyDaemon, conf_path)
|
|
# my-daemon section takes priority (!?)
|
|
self.assertEqual('2', d.conf['client_timeout'])
|
|
self.assertEqual('10', d.conf['CONN_timeout'])
|
|
self.assertEqual('5', d.conf['conn_timeout'])
|
|
|
|
@with_tempdir
|
|
def test_run_daemon_from_conf_file_with_duplicate_var(self, tempdir):
|
|
conf_path = os.path.join(tempdir, 'test-daemon.conf')
|
|
conf_body = """
|
|
[DEFAULT]
|
|
client_timeout = 3
|
|
[my-daemon]
|
|
CLIENT_TIMEOUT = 2
|
|
client_timeout = 1
|
|
conn_timeout = 1.1
|
|
conn_timeout = 1.2
|
|
"""
|
|
contents = dedent(conf_body)
|
|
with open(conf_path, 'w') as f:
|
|
f.write(contents)
|
|
with mock.patch('swift.common.utils.eventlet'), \
|
|
mock.patch('eventlet.hubs.use_hub'), \
|
|
mock.patch('eventlet.debug'):
|
|
app_config = lambda: daemon.run_daemon(MyDaemon, tempdir)
|
|
# N.B. CLIENT_TIMEOUT/client_timeout are unique options
|
|
self.assertDuplicateOption(app_config, 'conn_timeout', '1.2')
|
|
|
|
@with_tempdir
|
|
def test_run_deamon_from_conf_dir(self, tempdir):
|
|
conf_files = {
|
|
'default': """
|
|
[DEFAULT]
|
|
conn_timeout = 5
|
|
client_timeout = 1
|
|
""",
|
|
'daemon': """
|
|
[DEFAULT]
|
|
CONN_timeout = 3
|
|
CLIENT_TIMEOUT = 4
|
|
[my-daemon]
|
|
CONN_timeout = 10
|
|
client_timeout = 2
|
|
""",
|
|
}
|
|
for filename, conf_body in conf_files.items():
|
|
path = os.path.join(tempdir, filename + '.conf')
|
|
with open(path, 'wt') as fd:
|
|
fd.write(dedent(conf_body))
|
|
with mock.patch('swift.common.utils.eventlet'), \
|
|
mock.patch('eventlet.hubs.use_hub'), \
|
|
mock.patch('eventlet.debug'):
|
|
d = daemon.run_daemon(MyDaemon, tempdir)
|
|
# my-daemon section takes priority (!?)
|
|
self.assertEqual('2', d.conf['client_timeout'])
|
|
self.assertEqual('10', d.conf['CONN_timeout'])
|
|
self.assertEqual('5', d.conf['conn_timeout'])
|
|
|
|
@with_tempdir
|
|
def test_run_daemon_from_conf_dir_with_duplicate_var(self, tempdir):
|
|
conf_files = {
|
|
'default': """
|
|
[DEFAULT]
|
|
client_timeout = 3
|
|
""",
|
|
'daemon': """
|
|
[my-daemon]
|
|
client_timeout = 2
|
|
CLIENT_TIMEOUT = 4
|
|
conn_timeout = 1.1
|
|
conn_timeout = 1.2
|
|
""",
|
|
}
|
|
for filename, conf_body in conf_files.items():
|
|
path = os.path.join(tempdir, filename + '.conf')
|
|
with open(path, 'wt') as fd:
|
|
fd.write(dedent(conf_body))
|
|
with mock.patch('swift.common.utils.eventlet'), \
|
|
mock.patch('eventlet.hubs.use_hub'), \
|
|
mock.patch('eventlet.debug'):
|
|
app_config = lambda: daemon.run_daemon(MyDaemon, tempdir)
|
|
# N.B. CLIENT_TIMEOUT/client_timeout are unique options
|
|
self.assertDuplicateOption(app_config, 'conn_timeout', '1.2')
|
|
|
|
@contextmanager
|
|
def mock_os(self, child_worker_cycles=3):
|
|
self.waitpid_calls = defaultdict(int)
|
|
|
|
def mock_waitpid(p, *args):
|
|
self.waitpid_calls[p] += 1
|
|
if self.waitpid_calls[p] >= child_worker_cycles:
|
|
rv = p
|
|
else:
|
|
rv = 0
|
|
return rv, 0
|
|
with mock.patch('swift.common.daemon.os.fork') as mock_fork, \
|
|
mock.patch('swift.common.daemon.os.waitpid', mock_waitpid), \
|
|
mock.patch('swift.common.daemon.os.kill') as mock_kill:
|
|
mock_fork.side_effect = (
|
|
'mock-pid-%s' % i for i in itertools.count())
|
|
self.mock_fork = mock_fork
|
|
self.mock_kill = mock_kill
|
|
yield
|
|
|
|
def test_fork_workers(self):
|
|
utils.logging_monkey_patch() # needed to log at notice
|
|
d = MyWorkerDaemon({'workers': 3})
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
with self.mock_os():
|
|
strategy.run(once=True)
|
|
self.assertEqual([mock.call()] * 3, self.mock_fork.call_args_list)
|
|
self.assertEqual(self.waitpid_calls, {
|
|
'mock-pid-0': 3,
|
|
'mock-pid-1': 3,
|
|
'mock-pid-2': 3,
|
|
})
|
|
self.assertEqual([], self.mock_kill.call_args_list)
|
|
self.assertIn('Finished', d.logger.get_lines_for_level('notice')[-1])
|
|
self.assertTrue(MyWorkerDaemon.post_multiprocess_run_called)
|
|
|
|
def test_forked_worker(self):
|
|
d = MyWorkerDaemon({'workers': 3})
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
with mock.patch('swift.common.daemon.os.fork') as mock_fork, \
|
|
mock.patch('swift.common.daemon.os._exit') as mock_exit:
|
|
mock_fork.return_value = 0
|
|
mock_exit.side_effect = SystemExit
|
|
self.assertRaises(SystemExit, strategy.run, once=True)
|
|
self.assertTrue(d.once_called)
|
|
|
|
def test_restart_workers(self):
|
|
d = MyWorkerDaemon({'workers': 3})
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
d.health_side_effects = [True, False]
|
|
with self.mock_os():
|
|
self.mock_kill.side_effect = lambda *args, **kwargs: setattr(
|
|
strategy, 'running', False)
|
|
strategy.run()
|
|
# six workers forked in total
|
|
self.assertEqual([mock.call()] * 6, self.mock_fork.call_args_list)
|
|
# since the daemon starts healthy, first pass checks children once
|
|
self.assertEqual(self.waitpid_calls, {
|
|
'mock-pid-0': 1,
|
|
'mock-pid-1': 1,
|
|
'mock-pid-2': 1,
|
|
})
|
|
# second pass is not healthy, original pid's killed
|
|
self.assertEqual(set([
|
|
('mock-pid-0', signal.SIGTERM),
|
|
('mock-pid-1', signal.SIGTERM),
|
|
('mock-pid-2', signal.SIGTERM),
|
|
]), set(c[0] for c in self.mock_kill.call_args_list[:3]))
|
|
# our mock_kill side effect breaks out of running, and cleanup kills
|
|
# remaining pids
|
|
self.assertEqual(set([
|
|
('mock-pid-3', signal.SIGTERM),
|
|
('mock-pid-4', signal.SIGTERM),
|
|
('mock-pid-5', signal.SIGTERM),
|
|
]), set(c[0] for c in self.mock_kill.call_args_list[3:]))
|
|
|
|
def test_worker_disappears(self):
|
|
d = MyWorkerDaemon({'workers': 3})
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
strategy.register_worker_start('mock-pid', {'mock_options': True})
|
|
self.assertEqual(strategy.unspawned_worker_options, [])
|
|
self.assertEqual(strategy.options_by_pid, {
|
|
'mock-pid': {'mock_options': True}
|
|
})
|
|
# still running
|
|
with mock.patch('swift.common.daemon.os.waitpid') as mock_waitpid:
|
|
mock_waitpid.return_value = (0, 0)
|
|
strategy.check_on_all_running_workers()
|
|
self.assertEqual(strategy.unspawned_worker_options, [])
|
|
self.assertEqual(strategy.options_by_pid, {
|
|
'mock-pid': {'mock_options': True}
|
|
})
|
|
# finished
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
strategy.register_worker_start('mock-pid', {'mock_options': True})
|
|
with mock.patch('swift.common.daemon.os.waitpid') as mock_waitpid:
|
|
mock_waitpid.return_value = ('mock-pid', 0)
|
|
strategy.check_on_all_running_workers()
|
|
self.assertEqual(strategy.unspawned_worker_options, [
|
|
{'mock_options': True}])
|
|
self.assertEqual(strategy.options_by_pid, {})
|
|
self.assertEqual(d.logger.get_lines_for_level('debug')[-1],
|
|
'Worker mock-pid exited')
|
|
# disappeared
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
strategy.register_worker_start('mock-pid', {'mock_options': True})
|
|
with mock.patch('swift.common.daemon.os.waitpid') as mock_waitpid:
|
|
mock_waitpid.side_effect = OSError(
|
|
errno.ECHILD, os.strerror(errno.ECHILD))
|
|
mock_waitpid.return_value = ('mock-pid', 0)
|
|
strategy.check_on_all_running_workers()
|
|
self.assertEqual(strategy.unspawned_worker_options, [
|
|
{'mock_options': True}])
|
|
self.assertEqual(strategy.options_by_pid, {})
|
|
self.assertEqual(d.logger.get_lines_for_level('notice')[-1],
|
|
'Worker mock-pid died')
|
|
|
|
def test_worker_kills_pids_in_cleanup(self):
|
|
d = MyWorkerDaemon({'workers': 2})
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
strategy.register_worker_start('mock-pid-1', {'mock_options': True})
|
|
strategy.register_worker_start('mock-pid-2', {'mock_options': True})
|
|
self.assertEqual(strategy.unspawned_worker_options, [])
|
|
self.assertEqual(strategy.options_by_pid, {
|
|
'mock-pid-1': {'mock_options': True},
|
|
'mock-pid-2': {'mock_options': True},
|
|
})
|
|
with mock.patch('swift.common.daemon.os.kill') as mock_kill:
|
|
strategy.cleanup()
|
|
self.assertEqual(strategy.unspawned_worker_options, [
|
|
{'mock_options': True}] * 2)
|
|
self.assertEqual(strategy.options_by_pid, {})
|
|
self.assertEqual(set([
|
|
('mock-pid-1', signal.SIGTERM),
|
|
('mock-pid-2', signal.SIGTERM),
|
|
]), set(c[0] for c in mock_kill.call_args_list))
|
|
self.assertEqual(set(d.logger.get_lines_for_level('debug')[-2:]),
|
|
set(['Cleaned up worker mock-pid-1',
|
|
'Cleaned up worker mock-pid-2']))
|
|
|
|
def test_worker_disappears_in_cleanup(self):
|
|
d = MyWorkerDaemon({'workers': 2})
|
|
strategy = daemon.DaemonStrategy(d, d.logger)
|
|
strategy.register_worker_start('mock-pid-1', {'mock_options': True})
|
|
strategy.register_worker_start('mock-pid-2', {'mock_options': True})
|
|
self.assertEqual(strategy.unspawned_worker_options, [])
|
|
self.assertEqual(strategy.options_by_pid, {
|
|
'mock-pid-1': {'mock_options': True},
|
|
'mock-pid-2': {'mock_options': True},
|
|
})
|
|
with mock.patch('swift.common.daemon.os.kill') as mock_kill:
|
|
mock_kill.side_effect = [None, OSError(errno.ECHILD,
|
|
os.strerror(errno.ECHILD))]
|
|
strategy.cleanup()
|
|
self.assertEqual(strategy.unspawned_worker_options, [
|
|
{'mock_options': True}] * 2)
|
|
self.assertEqual(strategy.options_by_pid, {})
|
|
self.assertEqual(set([
|
|
('mock-pid-1', signal.SIGTERM),
|
|
('mock-pid-2', signal.SIGTERM),
|
|
]), set(c[0] for c in mock_kill.call_args_list))
|
|
self.assertEqual(set(d.logger.get_lines_for_level('debug')[-2:]),
|
|
set(['Cleaned up worker mock-pid-1',
|
|
'Cleaned up worker mock-pid-2']))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|