Skip to content

Commit 66fb0ec

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Initialize global data separately and run_once in WSGI app init" into stable/victoria
2 parents 4bb2338 + e3085fa commit 66fb0ec

File tree

5 files changed

+256
-2
lines changed

5 files changed

+256
-2
lines changed

nova/api/openstack/wsgi_app.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
CONFIG_FILES = ['api-paste.ini', 'nova.conf']
3131

32+
LOG = logging.getLogger(__name__)
33+
3234
objects.register_all()
3335

3436

@@ -77,8 +79,12 @@ def application(environ, start_response):
7779
return application
7880

7981

80-
def init_application(name):
81-
conf_files = _get_config_files()
82+
@utils.run_once('Global data already initialized, not re-initializing.',
83+
LOG.info)
84+
def init_global_data(conf_files):
85+
# NOTE(melwitt): parse_args initializes logging and calls global rpc.init()
86+
# and db_api.configure(). The db_api.configure() call does not initiate any
87+
# connection to the database.
8288
config.parse_args([], default_config_files=conf_files)
8389

8490
logging.setup(CONF, "nova")
@@ -93,11 +99,25 @@ def init_application(name):
9399
logging.getLogger(__name__),
94100
logging.DEBUG)
95101

102+
103+
def init_application(name):
104+
conf_files = _get_config_files()
105+
106+
# NOTE(melwitt): The init_application method can be called multiple times
107+
# within a single python interpreter instance if any exception is raised
108+
# during it (example: DBConnectionError while setting up the service) and
109+
# apache/mod_wsgi reloads the init_application script. So, we initialize
110+
# global data separately and decorate the method to run only once in a
111+
# python interpreter instance.
112+
init_global_data(conf_files)
113+
96114
try:
97115
_setup_service(CONF.host, name)
98116
except exception.ServiceTooOld as exc:
99117
return error_application(exc, name)
100118

119+
# This global init is safe because if we got here, we already successfully
120+
# set up the service and setting up the profile cannot fail.
101121
service.setup_profiler(name, CONF.host)
102122

103123
conf = conf_files[0]

nova/test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from sqlalchemy.dialects import sqlite
5555
import testtools
5656

57+
from nova.api.openstack import wsgi_app
5758
from nova.compute import rpcapi as compute_rpcapi
5859
from nova import context
5960
from nova.db.sqlalchemy import api as sqlalchemy_api
@@ -285,6 +286,10 @@ def setUp(self):
285286

286287
self.useFixture(nova_fixtures.GenericPoisonFixture())
287288

289+
# make sure that the wsgi app is fully initialized for all testcase
290+
# instead of only once initialized for test worker
291+
wsgi_app.init_global_data.reset()
292+
288293
def _setup_cells(self):
289294
"""Setup a normal cellsv2 environment.
290295
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import tempfile
14+
15+
import fixtures
16+
import mock
17+
from oslo_config import fixture as config_fixture
18+
from oslotest import base
19+
20+
from nova.api.openstack import wsgi_app
21+
from nova import test
22+
from nova.tests import fixtures as nova_fixtures
23+
24+
25+
class WSGIAppTest(base.BaseTestCase):
26+
27+
_paste_config = """
28+
[app:nova-api]
29+
use = egg:Paste#static
30+
document_root = /tmp
31+
"""
32+
33+
def setUp(self):
34+
# Ensure BaseTestCase's ConfigureLogging fixture is disabled since
35+
# we're using our own (StandardLogging).
36+
with fixtures.EnvironmentVariable('OS_LOG_CAPTURE', '0'):
37+
super(WSGIAppTest, self).setUp()
38+
self.stdlog = self.useFixture(nova_fixtures.StandardLogging())
39+
self.conf = tempfile.NamedTemporaryFile(mode='w+t')
40+
self.conf.write(self._paste_config.lstrip())
41+
self.conf.seek(0)
42+
self.conf.flush()
43+
self.addCleanup(self.conf.close)
44+
# Use of this fixture takes care of cleaning up config settings for
45+
# subsequent tests.
46+
self.useFixture(config_fixture.Config())
47+
48+
@mock.patch('sys.argv', return_value=mock.sentinel.argv)
49+
@mock.patch('nova.db.sqlalchemy.api.configure')
50+
@mock.patch('nova.api.openstack.wsgi_app._setup_service')
51+
@mock.patch('nova.api.openstack.wsgi_app._get_config_files')
52+
def test_init_application_called_twice(self, mock_get_files, mock_setup,
53+
mock_db_configure, mock_argv):
54+
"""Test that init_application can tolerate being called twice in a
55+
single python interpreter instance.
56+
57+
When nova-api is run via mod_wsgi, if any exception is raised during
58+
init_application, mod_wsgi will re-run the WSGI script without
59+
restarting the daemon process even when configured for Daemon Mode.
60+
61+
We access the database as part of init_application, so if nova-api
62+
starts up before the database is up, we'll get, for example, a
63+
DBConnectionError raised during init_application and our WSGI script
64+
will get reloaded/re-run by mod_wsgi.
65+
"""
66+
mock_get_files.return_value = [self.conf.name]
67+
mock_setup.side_effect = [test.TestingException, None]
68+
# We need to mock the global database configure() method, else we will
69+
# be affected by global database state altered by other tests that ran
70+
# before this test, causing this test to fail with
71+
# oslo_db.sqlalchemy.enginefacade.AlreadyStartedError. We can instead
72+
# mock the method to raise an exception if it's called a second time in
73+
# this test to simulate the fact that the database does not tolerate
74+
# re-init [after a database query has been made].
75+
mock_db_configure.side_effect = [None, test.TestingException]
76+
# Run init_application the first time, simulating an exception being
77+
# raised during it.
78+
self.assertRaises(test.TestingException, wsgi_app.init_application,
79+
'nova-api')
80+
# Now run init_application a second time, it should succeed since no
81+
# exception is being raised (the init of global data should not be
82+
# re-attempted).
83+
wsgi_app.init_application('nova-api')
84+
self.assertIn('Global data already initialized, not re-initializing.',
85+
self.stdlog.logger.output)

nova/tests/unit/test_utils.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,3 +1294,101 @@ def test_api_db_is_configured_but_the_service_cannot_access_db(
12941294
'not allowed to directly access the database. You should run this '
12951295
'service without the [api_database]/connection config option. The '
12961296
'service version check will only query the local cell.')
1297+
1298+
1299+
class RunOnceTests(test.NoDBTestCase):
1300+
1301+
fake_logger = mock.MagicMock()
1302+
1303+
@utils.run_once("already ran once", fake_logger)
1304+
def dummy_test_func(self, fail=False):
1305+
if fail:
1306+
raise ValueError()
1307+
return True
1308+
1309+
def setUp(self):
1310+
super(RunOnceTests, self).setUp()
1311+
self.dummy_test_func.reset()
1312+
RunOnceTests.fake_logger.reset_mock()
1313+
1314+
def test_wrapped_funtions_called_once(self):
1315+
self.assertFalse(self.dummy_test_func.called)
1316+
result = self.dummy_test_func()
1317+
self.assertTrue(result)
1318+
self.assertTrue(self.dummy_test_func.called)
1319+
1320+
# assert that on second invocation no result
1321+
# is returned and that the logger is invoked.
1322+
result = self.dummy_test_func()
1323+
RunOnceTests.fake_logger.assert_called_once()
1324+
self.assertIsNone(result)
1325+
1326+
def test_wrapped_funtions_called_once_raises(self):
1327+
self.assertFalse(self.dummy_test_func.called)
1328+
self.assertRaises(ValueError, self.dummy_test_func, fail=True)
1329+
self.assertTrue(self.dummy_test_func.called)
1330+
1331+
# assert that on second invocation no result
1332+
# is returned and that the logger is invoked.
1333+
result = self.dummy_test_func()
1334+
RunOnceTests.fake_logger.assert_called_once()
1335+
self.assertIsNone(result)
1336+
1337+
def test_wrapped_funtions_can_be_reset(self):
1338+
# assert we start with a clean state
1339+
self.assertFalse(self.dummy_test_func.called)
1340+
result = self.dummy_test_func()
1341+
self.assertTrue(result)
1342+
1343+
self.dummy_test_func.reset()
1344+
# assert we restored a clean state
1345+
self.assertFalse(self.dummy_test_func.called)
1346+
result = self.dummy_test_func()
1347+
self.assertTrue(result)
1348+
1349+
# assert that we never called the logger
1350+
RunOnceTests.fake_logger.assert_not_called()
1351+
1352+
def test_reset_calls_cleanup(self):
1353+
mock_clean = mock.Mock()
1354+
1355+
@utils.run_once("already ran once", self.fake_logger,
1356+
cleanup=mock_clean)
1357+
def f():
1358+
pass
1359+
1360+
f()
1361+
self.assertTrue(f.called)
1362+
1363+
f.reset()
1364+
self.assertFalse(f.called)
1365+
mock_clean.assert_called_once_with()
1366+
1367+
def test_clean_is_not_called_at_reset_if_wrapped_not_called(self):
1368+
mock_clean = mock.Mock()
1369+
1370+
@utils.run_once("already ran once", self.fake_logger,
1371+
cleanup=mock_clean)
1372+
def f():
1373+
pass
1374+
1375+
self.assertFalse(f.called)
1376+
1377+
f.reset()
1378+
self.assertFalse(f.called)
1379+
self.assertFalse(mock_clean.called)
1380+
1381+
def test_reset_works_even_if_cleanup_raises(self):
1382+
mock_clean = mock.Mock(side_effect=ValueError())
1383+
1384+
@utils.run_once("already ran once", self.fake_logger,
1385+
cleanup=mock_clean)
1386+
def f():
1387+
pass
1388+
1389+
f()
1390+
self.assertTrue(f.called)
1391+
1392+
self.assertRaises(ValueError, f.reset)
1393+
self.assertFalse(f.called)
1394+
mock_clean.assert_called_once_with()

nova/utils.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,3 +1103,49 @@ def raise_if_old_compute():
11031103
scope=scope,
11041104
min_service_level=current_service_version,
11051105
oldest_supported_service=oldest_supported_service_level)
1106+
1107+
1108+
def run_once(message, logger, cleanup=None):
1109+
"""This is a utility function decorator to ensure a function
1110+
is run once and only once in an interpreter instance.
1111+
1112+
Note: this is copied from the placement repo (placement/util.py)
1113+
1114+
The decorated function object can be reset by calling its
1115+
reset function. All exceptions raised by the wrapped function,
1116+
logger and cleanup function will be propagated to the caller.
1117+
"""
1118+
def outer_wrapper(func):
1119+
@functools.wraps(func)
1120+
def wrapper(*args, **kwargs):
1121+
if not wrapper.called:
1122+
# Note(sean-k-mooney): the called state is always
1123+
# updated even if the wrapped function completes
1124+
# by raising an exception. If the caller catches
1125+
# the exception it is their responsibility to call
1126+
# reset if they want to re-execute the wrapped function.
1127+
try:
1128+
return func(*args, **kwargs)
1129+
finally:
1130+
wrapper.called = True
1131+
else:
1132+
logger(message)
1133+
1134+
wrapper.called = False
1135+
1136+
def reset(wrapper, *args, **kwargs):
1137+
# Note(sean-k-mooney): we conditionally call the
1138+
# cleanup function if one is provided only when the
1139+
# wrapped function has been called previously. We catch
1140+
# and reraise any exception that may be raised and update
1141+
# the called state in a finally block to ensure its
1142+
# always updated if reset is called.
1143+
try:
1144+
if cleanup and wrapper.called:
1145+
return cleanup(*args, **kwargs)
1146+
finally:
1147+
wrapper.called = False
1148+
1149+
wrapper.reset = functools.partial(reset, wrapper)
1150+
return wrapper
1151+
return outer_wrapper

0 commit comments

Comments
 (0)