375 lines
15 KiB
Python
Raw Normal View History

# 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
from six import StringIO
Always set swift processes to use UTC Previously, we would set the TZ environment variable to the result of time.strftime("%z", time.gmtime()) This has a few problems. 1. The "%z" format does not appear in the table of formatting directives for strftime [1]. While it *does* appear in a footnote [2] for that section, it is described as "not supported by all ANSI C libraries." This may explain the next point. 2. On the handful of Linux platforms I've tested, the above produces "+0000" regardless of the system's timezone. This seems to run counter to the intent of the patches that introduced the TZ mangling. (See the first two related changes.) 3. The above does not produce a valid Posix TZ format, which expects (at minimum) a name consisting of three or more alphabetic characters followed by the offset to be added to the local time to get Coordinated Universal Time (UTC). Further, while we would change os.environ['TZ'], we would *not* call time.tzset like it says in the docs [3], which seems like a Bad Thing. Some combination of the above has the net effect of changing some of the functions in the time module to use UTC. (Maybe all of them? At the very least, time.localtime and time.mktime.) However, it does *not* change the offset stored in time.timezone, which causes bad behavior when dealing with local timestamps [4]. Now, set TZ to "UTC+0" and call tzset. Apparently we don't have a good way of getting local timezone info, we were (unintentionally?) using UTC before, and you should probably be running your servers in UTC anyway. [1] https://docs.python.org/2/library/time.html#time.strftime [2] https://docs.python.org/2/library/time.html#id2 [3] https://docs.python.org/2/library/time.html#time.tzset [4] Like in email.utils.mktime_tz, prior to being fixed in https://hg.python.org/cpython/rev/a283563c8cc4 Change-Id: I007425301914144e228b9cfece5533443e851b6e Related-Change: Ifc78236a99ed193a42389e383d062b38f57a5a31 Related-Change: I8ec80202789707f723abfe93ccc9cf1e677e4dc6 Related-Change: Iee7488d03ab404072d3d0c1a262f004bb0f2da26
2016-06-17 15:03:25 -07:00
import time
import unittest
from getpass import getuser
import logging
from test.unit import tmpfile
import mock
import signal
from contextlib import contextmanager
import itertools
from collections import defaultdict
import errno
from swift.common import daemon, utils
from test.unit import debug_logger
class MyDaemon(daemon.Daemon):
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.assertTrue(isinstance(d.logger, utils.LogAdapter))
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):
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.killpg(0, signal.SIGTERM),
# hard exit because bare except handlers can trap SystemExit
mock.call._exit(0)
])
def test_run_daemon(self):
sample_conf = "[my-daemon]\nuser = %s\n" % getuser()
with tmpfile(sample_conf) as conf_file, \
mock.patch('swift.common.daemon.use_hub') as mock_use_hub:
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)
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 = StringIO()
2011-02-02 09:38:17 -08:00
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.assertRaisesRegexp(SystemExit,
'Unable to find my-daemon '
'config section in.*',
daemon.run_daemon, MyDaemon,
conf_file, once=True)
Always set swift processes to use UTC Previously, we would set the TZ environment variable to the result of time.strftime("%z", time.gmtime()) This has a few problems. 1. The "%z" format does not appear in the table of formatting directives for strftime [1]. While it *does* appear in a footnote [2] for that section, it is described as "not supported by all ANSI C libraries." This may explain the next point. 2. On the handful of Linux platforms I've tested, the above produces "+0000" regardless of the system's timezone. This seems to run counter to the intent of the patches that introduced the TZ mangling. (See the first two related changes.) 3. The above does not produce a valid Posix TZ format, which expects (at minimum) a name consisting of three or more alphabetic characters followed by the offset to be added to the local time to get Coordinated Universal Time (UTC). Further, while we would change os.environ['TZ'], we would *not* call time.tzset like it says in the docs [3], which seems like a Bad Thing. Some combination of the above has the net effect of changing some of the functions in the time module to use UTC. (Maybe all of them? At the very least, time.localtime and time.mktime.) However, it does *not* change the offset stored in time.timezone, which causes bad behavior when dealing with local timestamps [4]. Now, set TZ to "UTC+0" and call tzset. Apparently we don't have a good way of getting local timezone info, we were (unintentionally?) using UTC before, and you should probably be running your servers in UTC anyway. [1] https://docs.python.org/2/library/time.html#time.strftime [2] https://docs.python.org/2/library/time.html#id2 [3] https://docs.python.org/2/library/time.html#time.tzset [4] Like in email.utils.mktime_tz, prior to being fixed in https://hg.python.org/cpython/rev/a283563c8cc4 Change-Id: I007425301914144e228b9cfece5533443e851b6e Related-Change: Ifc78236a99ed193a42389e383d062b38f57a5a31 Related-Change: I8ec80202789707f723abfe93ccc9cf1e677e4dc6 Related-Change: Iee7488d03ab404072d3d0c1a262f004bb0f2da26
2016-06-17 15:03:25 -07:00
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.daemon.use_hub'):
Always set swift processes to use UTC Previously, we would set the TZ environment variable to the result of time.strftime("%z", time.gmtime()) This has a few problems. 1. The "%z" format does not appear in the table of formatting directives for strftime [1]. While it *does* appear in a footnote [2] for that section, it is described as "not supported by all ANSI C libraries." This may explain the next point. 2. On the handful of Linux platforms I've tested, the above produces "+0000" regardless of the system's timezone. This seems to run counter to the intent of the patches that introduced the TZ mangling. (See the first two related changes.) 3. The above does not produce a valid Posix TZ format, which expects (at minimum) a name consisting of three or more alphabetic characters followed by the offset to be added to the local time to get Coordinated Universal Time (UTC). Further, while we would change os.environ['TZ'], we would *not* call time.tzset like it says in the docs [3], which seems like a Bad Thing. Some combination of the above has the net effect of changing some of the functions in the time module to use UTC. (Maybe all of them? At the very least, time.localtime and time.mktime.) However, it does *not* change the offset stored in time.timezone, which causes bad behavior when dealing with local timestamps [4]. Now, set TZ to "UTC+0" and call tzset. Apparently we don't have a good way of getting local timezone info, we were (unintentionally?) using UTC before, and you should probably be running your servers in UTC anyway. [1] https://docs.python.org/2/library/time.html#time.strftime [2] https://docs.python.org/2/library/time.html#id2 [3] https://docs.python.org/2/library/time.html#time.tzset [4] Like in email.utils.mktime_tz, prior to being fixed in https://hg.python.org/cpython/rev/a283563c8cc4 Change-Id: I007425301914144e228b9cfece5533443e851b6e Related-Change: Ifc78236a99ed193a42389e383d062b38f57a5a31 Related-Change: I8ec80202789707f723abfe93ccc9cf1e677e4dc6 Related-Change: Iee7488d03ab404072d3d0c1a262f004bb0f2da26
2016-06-17 15:03:25 -07:00
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()
@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):
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()