feat: shards mongodb driver + tests
This patch adds shard management capability to the queues mongodb driver. The storage API is also correctly split into an control and data interfaces. The data drivers are loaded by default. The control drivers are loaded as needed by the transport layer. Unit tests are also provided to verify that the driver (and future drivers) work as expected. Change-Id: Iad034a429a763c9a2ce161f05c928b090ab58944 Partially-implements: blueprint storage-sharding Partially-Closes: 1241686
This commit is contained in:
parent
142c7ae0d6
commit
9c7036ff4e
38
marconi/common/utils.py
Normal file
38
marconi/common/utils.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2013 Rackspace Hosting, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""utils: general-purpose utilities."""
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def fields(d, names, pred=lambda x: True,
|
||||
key_transform=lambda x: x, value_transform=lambda x: x):
|
||||
"""Returns the entries in this dictionary with keys appearing in names.
|
||||
:type d: dict
|
||||
:type names: [a]
|
||||
:param pred: a filter that is applied to the values of the dictionary.
|
||||
:type pred: (a -> bool)
|
||||
:param key_transform: a transform to apply to the key before returning it
|
||||
:type key_transform: a -> a
|
||||
:param value_transform: a transform to apply to the value before
|
||||
returning it
|
||||
:type value_transform: a -> a
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
return dict((key_transform(k), value_transform(v))
|
||||
for k, v in six.iteritems(d)
|
||||
if k in names and pred(v))
|
@ -58,7 +58,7 @@ class Bootstrap(object):
|
||||
self.driver_conf = self.conf[_DRIVER_GROUP]
|
||||
|
||||
log.setup('marconi')
|
||||
mode = 'admin' if self.conf.admin_mode else 'public'
|
||||
mode = 'admin' if conf.admin_mode else 'public'
|
||||
self._transport_type = 'marconi.queues.{0}.transport'.format(mode)
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
|
@ -9,3 +9,4 @@ DataDriverBase = base.DataDriverBase
|
||||
ClaimBase = base.ClaimBase
|
||||
MessageBase = base.MessageBase
|
||||
QueueBase = base.QueueBase
|
||||
ShardsBase = base.ShardsBase
|
||||
|
@ -368,8 +368,19 @@ class ClaimBase(ControllerBase):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AdminControllerBase(object):
|
||||
"""Top-level class for controllers.
|
||||
|
||||
:param driver: Instance of the driver
|
||||
instantiating this controller.
|
||||
"""
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ShardsController(ControllerBase):
|
||||
class ShardsBase(AdminControllerBase):
|
||||
"""A controller for managing shards."""
|
||||
|
||||
@abc.abstractmethod
|
||||
@ -404,11 +415,13 @@ class ShardsController(ControllerBase):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, name):
|
||||
def get(self, name, detailed=False):
|
||||
"""Returns a single shard entry.
|
||||
|
||||
:param name: The name of this shard
|
||||
:type name: six.text_type
|
||||
:param detailed: Should the options data be included?
|
||||
:type detailed: bool
|
||||
:returns: weight, uri, and options for this shard
|
||||
:rtype: {}
|
||||
:raises: ShardDoesNotExist if not found
|
||||
|
@ -28,6 +28,16 @@ from marconi.queues.storage.mongodb import options
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _connection(conf):
|
||||
"""MongoDB client connection instance."""
|
||||
if conf.uri and 'replicaSet' in conf.uri:
|
||||
MongoClient = pymongo.MongoReplicaSetClient
|
||||
else:
|
||||
MongoClient = pymongo.MongoClient
|
||||
|
||||
return MongoClient(conf.uri)
|
||||
|
||||
|
||||
class DataDriver(storage.DataDriverBase):
|
||||
|
||||
def __init__(self, conf):
|
||||
@ -68,14 +78,7 @@ class DataDriver(storage.DataDriverBase):
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
def connection(self):
|
||||
"""MongoDB client connection instance."""
|
||||
|
||||
if self.mongodb_conf.uri and 'replicaSet' in self.mongodb_conf.uri:
|
||||
MongoClient = pymongo.MongoReplicaSetClient
|
||||
else:
|
||||
MongoClient = pymongo.MongoClient
|
||||
|
||||
return MongoClient(self.mongodb_conf.uri)
|
||||
return _connection(self.mongodb_conf)
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
def queue_controller(self):
|
||||
@ -100,6 +103,16 @@ class ControlDriver(storage.ControlDriverBase):
|
||||
|
||||
self.mongodb_conf = self.conf[options.MONGODB_GROUP]
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
def connection(self):
|
||||
"""MongoDB client connection instance."""
|
||||
return _connection(self.mongodb_conf)
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
def shards_database(self):
|
||||
name = self.mongodb_conf.database + '_shards'
|
||||
return self.connection[name]
|
||||
|
||||
@property
|
||||
def shards_controller(self):
|
||||
return controllers.ShardsController(self)
|
||||
|
@ -14,28 +14,91 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from marconi.queues.storage import base
|
||||
"""shards: an implementation of the shard management storage
|
||||
controller for mongodb.
|
||||
|
||||
Schema:
|
||||
'n': name :: six.text_type
|
||||
'u': uri :: six.text_type
|
||||
'w': weight :: int
|
||||
'o': options :: dict
|
||||
"""
|
||||
|
||||
from marconi.common import utils as common_utils
|
||||
from marconi.queues.storage import base, exceptions
|
||||
from marconi.queues.storage.mongodb import utils
|
||||
|
||||
SHARDS_INDEX = [
|
||||
('n', 1)
|
||||
]
|
||||
|
||||
# NOTE(cpp-cabrera): used for get/list operations. There's no need to
|
||||
# show the marker or the _id - they're implementation details.
|
||||
OMIT_FIELDS = (('_id', 0),)
|
||||
|
||||
|
||||
class ShardsController(base.ShardsController):
|
||||
def _field_spec(detailed=False):
|
||||
return dict(OMIT_FIELDS + (() if detailed else (('o', 0),)))
|
||||
|
||||
|
||||
class ShardsController(base.ShardsBase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShardsController, self).__init__(*args, **kwargs)
|
||||
|
||||
self._col = self.driver.shards_database.shards
|
||||
self._col.ensure_index(SHARDS_INDEX,
|
||||
background=True,
|
||||
name='shards_name',
|
||||
unique=True)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def list(self, marker=None, limit=10, detailed=False):
|
||||
pass
|
||||
query = {}
|
||||
if marker is not None:
|
||||
query['n'] = {'$gt': marker}
|
||||
|
||||
def get(self, name):
|
||||
pass
|
||||
return self._col.find(query, fields=_field_spec(detailed),
|
||||
limit=limit)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def get(self, name, detailed=False):
|
||||
res = self._col.find_one({'n': name},
|
||||
_field_spec(detailed))
|
||||
if not res:
|
||||
raise exceptions.ShardDoesNotExist(name)
|
||||
return res
|
||||
|
||||
@utils.raises_conn_error
|
||||
def create(self, name, weight, uri, options=None):
|
||||
pass
|
||||
options = {} if options is None else options
|
||||
self._col.update({'n': name},
|
||||
{'$set': {'n': name, 'w': weight, 'u': uri,
|
||||
'o': options}},
|
||||
upsert=True)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def exists(self, name):
|
||||
pass
|
||||
return self._col.find_one({'n': name}) is not None
|
||||
|
||||
@utils.raises_conn_error
|
||||
def update(self, name, **kwargs):
|
||||
pass
|
||||
names = ('uri', 'weight', 'options')
|
||||
fields = common_utils.fields(kwargs, names,
|
||||
pred=lambda x: x is not None,
|
||||
key_transform=lambda x: x[0])
|
||||
assert fields, '`weight`, `uri`, or `options` not found in kwargs'
|
||||
res = self._col.update({'n': name},
|
||||
{'$set': fields},
|
||||
upsert=False)
|
||||
if not res['updatedExisting']:
|
||||
raise exceptions.ShardDoesNotExist(name)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def delete(self, name):
|
||||
pass
|
||||
self._col.remove({'n': name}, w=0)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def drop_all(self):
|
||||
pass
|
||||
self._col.drop()
|
||||
self._col.ensure_index(SHARDS_INDEX, unique=True)
|
||||
|
@ -144,10 +144,14 @@ class Catalog(object):
|
||||
# TODO(kgriffs): SHARDING - Read options from catalog backend
|
||||
conf = cfg.ConfigOpts()
|
||||
|
||||
general_opts = [
|
||||
cfg.BoolOpt('admin_mode', default=False)
|
||||
]
|
||||
options = [
|
||||
cfg.StrOpt('storage', default='sqlite')
|
||||
cfg.StrOpt('storage', default='sqlite'),
|
||||
]
|
||||
|
||||
conf.register_opts(general_opts)
|
||||
conf.register_opts(options, group='queues:drivers')
|
||||
return utils.load_storage_driver(conf)
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
from marconi.queues.storage import base
|
||||
|
||||
|
||||
class ShardsController(base.ShardsController):
|
||||
class ShardsController(base.ShardsBase):
|
||||
|
||||
def list(self, marker=None, limit=10, detailed=False):
|
||||
pass
|
||||
|
@ -32,7 +32,7 @@ def load_storage_driver(conf):
|
||||
"""
|
||||
|
||||
try:
|
||||
mgr = driver.DriverManager('marconi.queues.storage',
|
||||
mgr = driver.DriverManager('marconi.queues.data.storage',
|
||||
conf['queues:drivers'].storage,
|
||||
invoke_on_load=True,
|
||||
invoke_args=[conf])
|
||||
|
@ -17,6 +17,9 @@ from marconi.queues import storage
|
||||
|
||||
|
||||
class DataDriver(storage.DataDriverBase):
|
||||
def __init__(self, conf):
|
||||
super(DataDriver, self).__init__(conf)
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
return {}
|
||||
@ -35,9 +38,9 @@ class DataDriver(storage.DataDriverBase):
|
||||
|
||||
|
||||
class ControlDriver(storage.ControlDriverBase):
|
||||
@property
|
||||
def default_options(self):
|
||||
return {}
|
||||
|
||||
def __init__(self, conf):
|
||||
super(ControlDriver, self).__init__(conf)
|
||||
|
||||
@property
|
||||
def shards_controller(self):
|
||||
|
@ -601,6 +601,117 @@ class ClaimControllerTest(ControllerBaseTest):
|
||||
project=self.project)
|
||||
|
||||
|
||||
class ShardsControllerTest(ControllerBaseTest):
|
||||
"""Shards Controller base tests.
|
||||
|
||||
NOTE(flaper87): Implementations of this class should
|
||||
override the tearDown method in order
|
||||
to clean up storage's state.
|
||||
"""
|
||||
controller_base_class = storage.ShardsBase
|
||||
|
||||
def setUp(self):
|
||||
super(ShardsControllerTest, self).setUp()
|
||||
self.shards_controller = self.driver.shards_controller
|
||||
|
||||
# Let's create one shard
|
||||
self.shard = str(uuid.uuid1())
|
||||
self.shards_controller.create(self.shard, 100, 'localhost', {})
|
||||
|
||||
def tearDown(self):
|
||||
self.shards_controller.drop_all()
|
||||
super(ShardsControllerTest, self).tearDown()
|
||||
|
||||
def test_create_succeeds(self):
|
||||
self.shards_controller.create(str(uuid.uuid1()),
|
||||
100, 'localhost', {})
|
||||
|
||||
def test_create_replaces_on_duplicate_insert(self):
|
||||
name = str(uuid.uuid1())
|
||||
self.shards_controller.create(name,
|
||||
100, 'localhost', {})
|
||||
self.shards_controller.create(name,
|
||||
111, 'localhost2', {})
|
||||
entry = self.shards_controller.get(name)
|
||||
self._shard_expects(entry, xname=name, xweight=111,
|
||||
xlocation='localhost2')
|
||||
|
||||
def _shard_expects(self, shard, xname, xweight, xlocation):
|
||||
self.assertIn('n', shard)
|
||||
self.assertEqual(shard['n'], xname)
|
||||
self.assertIn('w', shard)
|
||||
self.assertEqual(shard['w'], xweight)
|
||||
self.assertIn('u', shard)
|
||||
self.assertEqual(shard['u'], xlocation)
|
||||
|
||||
def test_get_returns_expected_content(self):
|
||||
res = self.shards_controller.get(self.shard)
|
||||
self._shard_expects(res, self.shard, 100, 'localhost')
|
||||
self.assertNotIn('o', res)
|
||||
|
||||
def test_detailed_get_returns_expected_content(self):
|
||||
res = self.shards_controller.get(self.shard, detailed=True)
|
||||
self.assertIn('o', res)
|
||||
self.assertEqual(res['o'], {})
|
||||
|
||||
def test_get_raises_if_not_found(self):
|
||||
self.assertRaises(storage.exceptions.ShardDoesNotExist,
|
||||
self.shards_controller.get, 'notexists')
|
||||
|
||||
def test_exists(self):
|
||||
self.assertTrue(self.shards_controller.exists(self.shard))
|
||||
self.assertFalse(self.shards_controller.exists('notexists'))
|
||||
|
||||
def test_update_raises_assertion_error_on_bad_fields(self):
|
||||
self.assertRaises(AssertionError, self.shards_controller.update,
|
||||
self.shard)
|
||||
|
||||
def test_update_works(self):
|
||||
self.shards_controller.update(self.shard, weight=101,
|
||||
uri='redis://localhost',
|
||||
options={'a': 1})
|
||||
res = self.shards_controller.get(self.shard, detailed=True)
|
||||
self._shard_expects(res, self.shard, 101, 'redis://localhost')
|
||||
self.assertEqual(res['o'], {'a': 1})
|
||||
|
||||
def test_delete_works(self):
|
||||
self.shards_controller.delete(self.shard)
|
||||
self.assertFalse(self.shards_controller.exists(self.shard))
|
||||
|
||||
def test_delete_nonexistent_is_silent(self):
|
||||
self.shards_controller.delete('nonexisting')
|
||||
|
||||
def test_drop_all_leads_to_empty_listing(self):
|
||||
self.shards_controller.drop_all()
|
||||
cursor = self.shards_controller.list()
|
||||
self.assertRaises(StopIteration, next, cursor)
|
||||
|
||||
def test_listing_simple(self):
|
||||
# NOTE(cpp-cabrera): base entry interferes with listing results
|
||||
self.shards_controller.delete(self.shard)
|
||||
|
||||
for i in range(15):
|
||||
self.shards_controller.create(str(i), i, str(i), {})
|
||||
|
||||
res = list(self.shards_controller.list())
|
||||
self.assertEqual(len(res), 10)
|
||||
for i, entry in enumerate(res):
|
||||
self._shard_expects(entry, str(i), i, str(i))
|
||||
|
||||
res = list(self.shards_controller.list(limit=5))
|
||||
self.assertEqual(len(res), 5)
|
||||
|
||||
res = next(self.shards_controller.list(marker='3'))
|
||||
self._shard_expects(res, '4', 4, '4')
|
||||
|
||||
res = list(self.shards_controller.list(detailed=True))
|
||||
self.assertEqual(len(res), 10)
|
||||
for i, entry in enumerate(res):
|
||||
self._shard_expects(entry, str(i), i, str(i))
|
||||
self.assertIn('o', entry)
|
||||
self.assertEqual(entry['o'], {})
|
||||
|
||||
|
||||
def _insert_fixtures(controller, queue_name, project=None,
|
||||
client_uuid=None, num=4, ttl=120):
|
||||
|
||||
|
@ -26,10 +26,14 @@ packages =
|
||||
console_scripts =
|
||||
marconi-server = marconi.cmd.server:run
|
||||
|
||||
marconi.queues.storage =
|
||||
marconi.queues.data.storage =
|
||||
sqlite = marconi.queues.storage.sqlite.driver:DataDriver
|
||||
mongodb = marconi.queues.storage.mongodb.driver:DataDriver
|
||||
|
||||
marconi.queues.control.storage =
|
||||
sqlite = marconi.queues.storage.sqlite.driver:ControlDriver
|
||||
mongodb = marconi.queues.storage.mongodb.driver:ControlDriver
|
||||
|
||||
marconi.queues.public.transport =
|
||||
wsgi = marconi.queues.transport.wsgi.public.driver:Driver
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
[DEFAULT]
|
||||
debug = False
|
||||
verbose = False
|
||||
admin_mode = False
|
||||
|
||||
[queues:drivers]
|
||||
transport = wsgi
|
||||
|
@ -1,6 +1,7 @@
|
||||
[DEFAULT]
|
||||
debug = False
|
||||
verbose = False
|
||||
admin_mode = False
|
||||
|
||||
[queues:drivers]
|
||||
transport = wsgi
|
||||
|
@ -332,3 +332,16 @@ class MongodbClaimTests(base.ClaimControllerTest):
|
||||
self.assertRaises(storage.exceptions.ClaimDoesNotExist,
|
||||
self.controller.update, self.queue_name,
|
||||
claim_id, {}, project=self.project)
|
||||
|
||||
|
||||
@testing.requires_mongodb
|
||||
class MongodbShardsTests(base.ShardsControllerTest):
|
||||
driver_class = mongodb.ControlDriver
|
||||
controller_class = controllers.ShardsController
|
||||
|
||||
def setUp(self):
|
||||
super(MongodbShardsTests, self).setUp()
|
||||
self.load_conf('wsgi_mongodb.conf')
|
||||
|
||||
def tearDown(self):
|
||||
super(MongodbShardsTests, self).tearDown()
|
||||
|
@ -29,7 +29,7 @@ class TestShardCatalog(base.TestBase):
|
||||
conf_file = 'etc/wsgi_sqlite_sharded.conf'
|
||||
|
||||
conf = cfg.ConfigOpts()
|
||||
conf(default_config_files=[conf_file])
|
||||
conf(args=[], default_config_files=[conf_file])
|
||||
|
||||
lookup = sharding.Catalog(conf).lookup
|
||||
|
||||
|
@ -25,8 +25,8 @@ from marconi.tests import base
|
||||
class TestBootstrap(base.TestBase):
|
||||
|
||||
def _bootstrap(self, conf_file):
|
||||
conf = self.load_conf(conf_file)
|
||||
return bootstrap.Bootstrap(conf)
|
||||
self.conf = self.load_conf(conf_file)
|
||||
return bootstrap.Bootstrap(self.conf)
|
||||
|
||||
def test_storage_invalid(self):
|
||||
boot = self._bootstrap('etc/drivers_storage_invalid.conf')
|
||||
|
Loading…
Reference in New Issue
Block a user