Add first reliability enforcement

This patch adds an reliability enforcement for mongodb's driver. It
forces deployers to use replicasets or mongos as a mongodb cluster for
Zaqar. In addition to that, it forces deployers to provide a write
concern > 2 and/or majority.

If none of this are met, the driver will raise a RuntimeException and
fail to start. If no write concern is provided, majority will be used.

Change-Id: Ie74a4b441654243b3ed7e7fd6c40863969cd446d
Closes-bug: #1372335
This commit is contained in:
Flavio Percoco 2014-09-24 16:51:38 +02:00
parent 2d3ee77efe
commit a1163331fc
14 changed files with 138 additions and 18 deletions

View File

@ -1,6 +1,7 @@
[DEFAULT] [DEFAULT]
debug = False debug = False
verbose = False verbose = False
unreliable = True
[drivers] [drivers]
transport = wsgi transport = wsgi

View File

@ -1,6 +1,7 @@
[DEFAULT] [DEFAULT]
pooling = True pooling = True
admin_mode = True admin_mode = True
unreliable = True
[drivers] [drivers]
transport = wsgi transport = wsgi

View File

@ -15,6 +15,7 @@
import ddt import ddt
from zaqar.queues import bootstrap
from zaqar.queues.storage import utils from zaqar.queues.storage import utils
from zaqar import tests as testing from zaqar import tests as testing
@ -22,9 +23,15 @@ from zaqar import tests as testing
@ddt.ddt @ddt.ddt
class TestUtils(testing.TestBase): class TestUtils(testing.TestBase):
def setUp(self):
super(TestUtils, self).setUp()
self.conf.register_opts(bootstrap._GENERAL_OPTIONS)
@testing.requires_mongodb @testing.requires_mongodb
def test_can_connect_suceeds_if_good_uri_mongo(self): def test_can_connect_suceeds_if_good_uri_mongo(self):
self.assertTrue(utils.can_connect('mongodb://localhost:27017')) self.config(unreliable=True)
self.assertTrue(utils.can_connect('mongodb://localhost:27017',
conf=self.conf))
@testing.requires_redis @testing.requires_redis
def test_can_connect_suceeds_if_good_uri_redis(self): def test_can_connect_suceeds_if_good_uri_redis(self):
@ -39,8 +46,11 @@ class TestUtils(testing.TestBase):
@testing.requires_mongodb @testing.requires_mongodb
def test_can_connect_fails_if_bad_uri_mongodb(self): def test_can_connect_fails_if_bad_uri_mongodb(self):
self.assertFalse(utils.can_connect('mongodb://localhost:8080')) self.config(unreliable=True)
self.assertFalse(utils.can_connect('mongodb://example.com:27017')) self.assertFalse(utils.can_connect('mongodb://localhost:8080',
conf=self.conf))
self.assertFalse(utils.can_connect('mongodb://example.com:27017',
conf=self.conf))
@testing.requires_redis @testing.requires_redis
def test_can_connect_fails_if_bad_uri_redis(self): def test_can_connect_fails_if_bad_uri_redis(self):

View File

@ -26,6 +26,7 @@ import six
from testtools import matchers from testtools import matchers
from zaqar.openstack.common.cache import cache as oslo_cache from zaqar.openstack.common.cache import cache as oslo_cache
from zaqar.queues import bootstrap
from zaqar.queues import storage from zaqar.queues import storage
from zaqar.queues.storage import errors from zaqar.queues.storage import errors
from zaqar.queues.storage import mongodb from zaqar.queues.storage import mongodb
@ -140,7 +141,14 @@ class MongodbDriverTest(MongodbSetupMixin, testing.TestBase):
config_file = 'wsgi_mongodb.conf' config_file = 'wsgi_mongodb.conf'
def setUp(self):
super(MongodbDriverTest, self).setUp()
self.conf.register_opts(bootstrap._GENERAL_OPTIONS)
self.config(unreliable=False)
def test_db_instance(self): def test_db_instance(self):
self.config(unreliable=True)
cache = oslo_cache.get_cache() cache = oslo_cache.get_cache()
driver = mongodb.DataDriver(self.conf, cache) driver = mongodb.DataDriver(self.conf, cache)
@ -152,6 +160,7 @@ class MongodbDriverTest(MongodbSetupMixin, testing.TestBase):
driver.mongodb_conf.database)) driver.mongodb_conf.database))
def test_version_match(self): def test_version_match(self):
self.config(unreliable=True)
cache = oslo_cache.get_cache() cache = oslo_cache.get_cache()
with mock.patch('pymongo.MongoClient.server_info') as info: with mock.patch('pymongo.MongoClient.server_info') as info:
@ -166,6 +175,54 @@ class MongodbDriverTest(MongodbSetupMixin, testing.TestBase):
except RuntimeError: except RuntimeError:
self.fail('version match failed') self.fail('version match failed')
def test_replicaset_or_mongos_needed(self):
cache = oslo_cache.get_cache()
with mock.patch('pymongo.MongoClient.nodes') as nodes:
nodes.__get__ = mock.Mock(return_value=[])
with mock.patch('pymongo.MongoClient.is_mongos') as is_mongos:
is_mongos.__get__ = mock.Mock(return_value=False)
self.assertRaises(RuntimeError, mongodb.DataDriver,
self.conf, cache)
def test_using_replset(self):
cache = oslo_cache.get_cache()
with mock.patch('pymongo.MongoClient.nodes') as nodes:
nodes.__get__ = mock.Mock(return_value=['node1', 'node2'])
mongodb.DataDriver(self.conf, cache)
def test_using_mongos(self):
cache = oslo_cache.get_cache()
with mock.patch('pymongo.MongoClient.is_mongos') as is_mongos:
is_mongos.__get__ = mock.Mock(return_value=True)
mongodb.DataDriver(self.conf, cache)
def test_write_concern_check_works(self):
cache = oslo_cache.get_cache()
with mock.patch('pymongo.MongoClient.is_mongos') as is_mongos:
is_mongos.__get__ = mock.Mock(return_value=True)
with mock.patch('pymongo.MongoClient.write_concern') as wc:
wc.__get__ = mock.Mock(return_value={'w': 1})
self.assertRaises(RuntimeError, mongodb.DataDriver,
self.conf, cache)
wc.__get__ = mock.Mock(return_value={'w': 2})
mongodb.DataDriver(self.conf, cache)
def test_write_concern_is_set(self):
cache = oslo_cache.get_cache()
with mock.patch('pymongo.MongoClient.is_mongos') as is_mongos:
is_mongos.__get__ = mock.Mock(return_value=True)
driver = mongodb.DataDriver(self.conf, cache)
wc = driver.connection.write_concern
self.assertEqual(wc['w'], 'majority')
self.assertEqual(wc['j'], False)
@testing.requires_mongodb @testing.requires_mongodb
class MongodbQueueTests(MongodbSetupMixin, base.QueueControllerTest): class MongodbQueueTests(MongodbSetupMixin, base.QueueControllerTest):

View File

@ -27,17 +27,19 @@ from zaqar import tests as testing
@testing.requires_mongodb @testing.requires_mongodb
class PoolQueuesTest(testing.TestBase): class PoolQueuesTest(testing.TestBase):
config_file = 'wsgi_mongodb_pooled.conf'
def setUp(self): def setUp(self):
super(PoolQueuesTest, self).setUp() super(PoolQueuesTest, self).setUp()
conf = self.load_conf('wsgi_mongodb_pooled.conf')
conf.register_opts([cfg.StrOpt('storage')], self.conf.register_opts([cfg.StrOpt('storage')],
group='drivers') group='drivers')
cache = oslo_cache.get_cache() cache = oslo_cache.get_cache()
control = utils.load_storage_driver(conf, cache, control_mode=True) control = utils.load_storage_driver(self.conf, cache,
control_mode=True)
self.pools_ctrl = control.pools_controller self.pools_ctrl = control.pools_controller
self.driver = pooling.DataDriver(conf, cache, control) self.driver = pooling.DataDriver(self.conf, cache, control)
self.controller = self.driver.queue_controller self.controller = self.driver.queue_controller
# fake two pools # fake two pools

View File

@ -18,6 +18,7 @@ from stevedore import driver
from zaqar.common import decorators from zaqar.common import decorators
from zaqar.common import errors from zaqar.common import errors
from zaqar.i18n import _
from zaqar.openstack.common.cache import cache as oslo_cache from zaqar.openstack.common.cache import cache as oslo_cache
from zaqar.openstack.common import log from zaqar.openstack.common import log
from zaqar.queues.storage import pipeline from zaqar.queues.storage import pipeline
@ -49,6 +50,9 @@ _GENERAL_OPTIONS = (
'configuration is used to determine where the ' 'configuration is used to determine where the '
'catalogue/control plane data is kept.'), 'catalogue/control plane data is kept.'),
deprecated_opts=[cfg.DeprecatedOpt('pooling')]), deprecated_opts=[cfg.DeprecatedOpt('pooling')]),
cfg.BoolOpt('unreliable', default=None,
help=('Disable all reliability constrains.')),
) )
_DRIVER_OPTIONS = ( _DRIVER_OPTIONS = (
@ -81,6 +85,14 @@ class Bootstrap(object):
log.setup('zaqar') log.setup('zaqar')
if self.conf.unreliable is None:
msg = _(u'Unreliable\'s default value will be changed to False '
'in the Kilo release. Please, make sure your deployments '
'are working in a reliable mode or that `unreliable` is '
'explicitly set to `True` in your configuration files.')
LOG.warn(msg)
self.conf.unreliable = True
@decorators.lazy_property(write=False) @decorators.lazy_property(write=False)
def storage(self): def storage(self):
LOG.debug(u'Loading storage driver') LOG.debug(u'Loading storage driver')

View File

@ -72,11 +72,36 @@ class DataDriver(storage.DataDriverBase):
self.mongodb_conf = self.conf[options.MONGODB_GROUP] self.mongodb_conf = self.conf[options.MONGODB_GROUP]
server_version = self.connection.server_info()['version'] conn = self.connection
server_version = conn.server_info()['version']
if tuple(map(int, server_version.split('.'))) < (2, 2): if tuple(map(int, server_version.split('.'))) < (2, 2):
raise RuntimeError(_('The mongodb driver requires mongodb>=2.2, ' raise RuntimeError(_('The mongodb driver requires mongodb>=2.2, '
'%s found') % server_version) '%s found') % server_version)
if not len(conn.nodes) > 1 and not conn.is_mongos:
if not self.conf.unreliable:
raise RuntimeError(_('Either a replica set or a mongos is '
'required to guarantee message delivery'))
else:
wc = conn.write_concern.get('w')
majority = (wc == 'majority' or
wc >= 2)
if not wc:
# NOTE(flaper87): No write concern specified, use majority
# and don't count journal as a replica. Use `update` to avoid
# overwriting `wtimeout`
conn.write_concern.update({'w': 'majority'})
elif not self.conf.unreliable and not majority:
raise RuntimeError(_('Using a write concern other than '
'`majority` or > 2 make sthe service '
'unreliable. Please use a different '
'write concern or set `unreliable` '
'to True in the config file.'))
conn.write_concern['j'] = False
def is_alive(self): def is_alive(self):
try: try:
# NOTE(zyuan): Requires admin access to mongodb # NOTE(zyuan): Requires admin access to mongodb

View File

@ -399,7 +399,8 @@ class Catalog(object):
:rtype: zaqar.queues.storage.base.DataDriverBase :rtype: zaqar.queues.storage.base.DataDriverBase
""" """
pool = self._pools_ctrl.get(pool_id, detailed=True) pool = self._pools_ctrl.get(pool_id, detailed=True)
conf = utils.dynamic_conf(pool['uri'], pool['options']) conf = utils.dynamic_conf(pool['uri'], pool['options'],
conf=self._conf)
return utils.load_storage_driver(conf, self._cache) return utils.load_storage_driver(conf, self._cache)
@decorators.caches(_pool_cache_key, _POOL_CACHE_TTL) @decorators.caches(_pool_cache_key, _POOL_CACHE_TTL)

View File

@ -49,7 +49,7 @@ def dynamic_conf(uri, options, conf=None):
if conf is None: if conf is None:
conf = cfg.ConfigOpts() conf = cfg.ConfigOpts()
else: else:
conf = copy.deepcopy(conf) conf = copy.copy(conf)
if storage_group not in conf: if storage_group not in conf:
conf.register_opts(storage_opts, conf.register_opts(storage_opts,
@ -60,6 +60,7 @@ def dynamic_conf(uri, options, conf=None):
driver_opts = utils.dict_to_conf({'storage': storage_type}) driver_opts = utils.dict_to_conf({'storage': storage_type})
conf.register_opts(driver_opts, group=u'drivers') conf.register_opts(driver_opts, group=u'drivers')
conf.set_override('storage', storage_type, 'drivers')
conf.set_override('uri', uri, group=storage_group) conf.set_override('uri', uri, group=storage_group)
return conf return conf

View File

@ -150,9 +150,10 @@ class Resource(object):
LOG.debug(u'PUT pool - name: %s', pool) LOG.debug(u'PUT pool - name: %s', pool)
conf = self._ctrl.driver.conf
data = wsgi_utils.load(request) data = wsgi_utils.load(request)
wsgi_utils.validate(self._validators['create'], data) wsgi_utils.validate(self._validators['create'], data)
if not storage_utils.can_connect(data['uri']): if not storage_utils.can_connect(data['uri'], conf=conf):
raise wsgi_errors.HTTPBadRequestBody( raise wsgi_errors.HTTPBadRequestBody(
'cannot connect to %s' % data['uri'] 'cannot connect to %s' % data['uri']
) )
@ -199,7 +200,9 @@ class Resource(object):
for field in EXPECT: for field in EXPECT:
wsgi_utils.validate(self._validators[field], data) wsgi_utils.validate(self._validators[field], data)
if 'uri' in data and not storage_utils.can_connect(data['uri']): conf = self._ctrl.driver.conf
if 'uri' in data and not storage_utils.can_connect(data['uri'],
conf=conf):
raise wsgi_errors.HTTPBadRequestBody( raise wsgi_errors.HTTPBadRequestBody(
'cannot connect to %s' % data['uri'] 'cannot connect to %s' % data['uri']
) )

View File

@ -152,9 +152,10 @@ class Resource(object):
LOG.debug(u'PUT pool - name: %s', pool) LOG.debug(u'PUT pool - name: %s', pool)
conf = self._ctrl.driver.conf
data = wsgi_utils.load(request) data = wsgi_utils.load(request)
wsgi_utils.validate(self._validators['create'], data) wsgi_utils.validate(self._validators['create'], data)
if not storage_utils.can_connect(data['uri']): if not storage_utils.can_connect(data['uri'], conf=conf):
raise wsgi_errors.HTTPBadRequestBody( raise wsgi_errors.HTTPBadRequestBody(
'cannot connect to %s' % data['uri'] 'cannot connect to %s' % data['uri']
) )
@ -212,7 +213,9 @@ class Resource(object):
for field in EXPECT: for field in EXPECT:
wsgi_utils.validate(self._validators[field], data) wsgi_utils.validate(self._validators[field], data)
if 'uri' in data and not storage_utils.can_connect(data['uri']): conf = self._ctrl.driver.conf
if 'uri' in data and not storage_utils.can_connect(data['uri'],
conf=conf):
raise wsgi_errors.HTTPBadRequestBody( raise wsgi_errors.HTTPBadRequestBody(
'cannot connect to %s' % data['uri'] 'cannot connect to %s' % data['uri']
) )

View File

@ -20,6 +20,8 @@ from oslo.config import cfg
import six import six
import testtools import testtools
from zaqar.queues import bootstrap
class TestBase(testtools.TestCase): class TestBase(testtools.TestCase):
"""Child class of testtools.TestCase for testing Zaqar. """Child class of testtools.TestCase for testing Zaqar.
@ -46,6 +48,8 @@ class TestBase(testtools.TestCase):
else: else:
self.conf = cfg.ConfigOpts() self.conf = cfg.ConfigOpts()
self.conf.register_opts(bootstrap._GENERAL_OPTIONS)
@classmethod @classmethod
def conf_path(cls, filename): def conf_path(cls, filename):
"""Returns the full path to the specified Zaqar conf file. """Returns the full path to the specified Zaqar conf file.

View File

@ -24,7 +24,6 @@ import six
from testtools import matchers from testtools import matchers
from zaqar.openstack.common.cache import cache as oslo_cache from zaqar.openstack.common.cache import cache as oslo_cache
from zaqar.queues import bootstrap
from zaqar.queues import storage from zaqar.queues import storage
from zaqar.queues.storage import errors from zaqar.queues.storage import errors
from zaqar import tests as testing from zaqar import tests as testing
@ -52,7 +51,6 @@ class ControllerBaseTest(testing.TestBase):
oslo_cache.register_oslo_configs(self.conf) oslo_cache.register_oslo_configs(self.conf)
cache = oslo_cache.get_cache(self.conf.cache_url) cache = oslo_cache.get_cache(self.conf.cache_url)
self.conf.register_opts(bootstrap._GENERAL_OPTIONS)
pooling = 'pooling' in self.conf and self.conf.pooling pooling = 'pooling' in self.conf and self.conf.pooling
if pooling and not self.control_driver_class: if pooling and not self.control_driver_class:
self.skipTest("Pooling is enabled, " self.skipTest("Pooling is enabled, "

View File

@ -33,6 +33,7 @@ class TestBase(testing.TestBase):
if not self.config_file: if not self.config_file:
self.skipTest("No config specified") self.skipTest("No config specified")
self.conf.register_opts(bootstrap._GENERAL_OPTIONS)
self.conf.register_opts(validation._TRANSPORT_LIMITS_OPTIONS, self.conf.register_opts(validation._TRANSPORT_LIMITS_OPTIONS,
group=validation._TRANSPORT_LIMITS_GROUP) group=validation._TRANSPORT_LIMITS_GROUP)
self.transport_cfg = self.conf[validation._TRANSPORT_LIMITS_GROUP] self.transport_cfg = self.conf[validation._TRANSPORT_LIMITS_GROUP]
@ -41,6 +42,7 @@ class TestBase(testing.TestBase):
group=driver._WSGI_GROUP) group=driver._WSGI_GROUP)
self.wsgi_cfg = self.conf[driver._WSGI_GROUP] self.wsgi_cfg = self.conf[driver._WSGI_GROUP]
self.conf.unreliable = True
self.conf.admin_mode = True self.conf.admin_mode = True
self.boot = bootstrap.Bootstrap(self.conf) self.boot = bootstrap.Bootstrap(self.conf)