zaqar/zaqar/storage/mongodb/driver.py

332 lines
12 KiB
Python

# Copyright (c) 2013 Red Hat, 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.
"""Mongodb storage driver implementation."""
import ssl
from osprofiler import profiler
import pymongo
import pymongo.errors
from zaqar.common import decorators
from zaqar.conf import drivers_management_store_mongodb
from zaqar.conf import drivers_message_store_mongodb
from zaqar.i18n import _
from zaqar import storage
from zaqar.storage.mongodb import controllers
def _connection(conf):
# NOTE(flaper87): remove possible zaqar specific
# schemes like: mongodb.fifo
uri = conf.uri
if conf.uri:
uri = "mongodb://%s" % (conf.uri.split("://")[-1])
if conf.uri and 'replicaSet' in conf.uri:
MongoClient = pymongo.MongoReplicaSetClient
else:
MongoClient = pymongo.MongoClient
if conf.uri and 'ssl=true' in conf.uri.lower():
kwargs = {'connect': False}
# Default to CERT_REQUIRED
ssl_cert_reqs = ssl.CERT_REQUIRED
if conf.ssl_cert_reqs == 'CERT_OPTIONAL':
ssl_cert_reqs = ssl.CERT_OPTIONAL
if conf.ssl_cert_reqs == 'CERT_NONE':
ssl_cert_reqs = ssl.CERT_NONE
kwargs['ssl_cert_reqs'] = ssl_cert_reqs
if conf.ssl_keyfile:
kwargs['ssl_keyfile'] = conf.ssl_keyfile
if conf.ssl_certfile:
kwargs['ssl_certfile'] = conf.ssl_certfile
if conf.ssl_ca_certs:
kwargs['ssl_ca_certs'] = conf.ssl_ca_certs
return MongoClient(uri, **kwargs)
return MongoClient(uri, connect=False)
class DataDriver(storage.DataDriverBase):
BASE_CAPABILITIES = tuple(storage.Capabilities)
_DRIVER_OPTIONS = [(drivers_management_store_mongodb.GROUP_NAME,
drivers_management_store_mongodb.ALL_OPTS),
(drivers_message_store_mongodb.GROUP_NAME,
drivers_message_store_mongodb.ALL_OPTS)]
_COL_SUFIX = "_messages_p"
def __init__(self, conf, cache, control_driver):
super(DataDriver, self).__init__(conf, cache, control_driver)
self.mongodb_conf = self.conf[drivers_message_store_mongodb.GROUP_NAME]
conn = self.connection
server_info = conn.server_info()['version']
self.server_version = tuple(map(int, server_info.split('.')))
if self.server_version < (2, 2):
raise RuntimeError(_('The mongodb driver requires mongodb>=2.2, '
'%s found') % server_info)
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:
_mongo_wc = conn.write_concern.document.get('w')
# NOTE(flwang): mongo client is using None as the default value of
# write concern. But in Python 3.x we can't compare by order
# different types of operands like in Python 2.x.
# And we can't set the write concern value when create the
# connection since it will fail with norepl if mongodb version
# below 2.6. Besides it doesn't make sense to create the
# connection again after getting the version.
durable = (_mongo_wc is not None and
(_mongo_wc == 'majority' or _mongo_wc >= 2)
)
if not self.conf.unreliable and not durable:
raise RuntimeError(_('Using a write concern other than '
'`majority` or > 2 makes the service '
'unreliable. Please use a different '
'write concern or set `unreliable` '
'to True in the config file.'))
# FIXME(flaper87): Make this dynamic
self._capabilities = self.BASE_CAPABILITIES
@property
def capabilities(self):
return self._capabilities
def is_alive(self):
try:
# NOTE(zyuan): Requires admin access to mongodb
return 'ok' in self.connection.admin.command('ping')
except pymongo.errors.PyMongoError:
return False
def close(self):
self.connection.close()
def _health(self):
KPI = {}
KPI['storage_reachable'] = self.is_alive()
KPI['operation_status'] = self._get_operation_status()
message_volume = {'free': 0, 'claimed': 0, 'total': 0}
for msg_col in [db.messages for db in self.message_databases]:
msg_count_claimed = msg_col.find({'c.id': {'$ne': None}}).count()
message_volume['claimed'] += msg_count_claimed
msg_count_total = msg_col.find().count()
message_volume['total'] += msg_count_total
message_volume['free'] = (message_volume['total'] -
message_volume['claimed'])
KPI['message_volume'] = message_volume
return KPI
@decorators.lazy_property(write=False)
def message_databases(self):
"""List of message databases, ordered by partition number."""
kwargs = {}
if not self.server_version < (2, 6):
# NOTE(flaper87): Skip mongodb versions below 2.6 when
# setting the write concern on the database. pymongo 3.0
# fails with norepl when creating indexes.
doc = self.connection.write_concern.document.copy()
doc.setdefault('w', 'majority')
doc.setdefault('j', False)
kwargs['write_concern'] = pymongo.WriteConcern(**doc)
name = self.mongodb_conf.database
partitions = self.mongodb_conf.partitions
databases = []
for p in range(partitions):
db_name = name + self._COL_SUFIX + str(p)
databases.append(self.connection.get_database(db_name, **kwargs))
return databases
@decorators.lazy_property(write=False)
def subscriptions_database(self):
"""Database dedicated to the "subscription" collection."""
name = self.mongodb_conf.database + '_subscriptions'
return self.connection[name]
@decorators.lazy_property(write=False)
def connection(self):
"""MongoDB client connection instance."""
return _connection(self.mongodb_conf)
@decorators.lazy_property(write=False)
def message_controller(self):
controller = controllers.MessageController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_message_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def claim_controller(self):
controller = controllers.ClaimController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_claim_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def subscription_controller(self):
controller = controllers.SubscriptionController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_subscription_"
"controller")(controller)
else:
return controller
class FIFODataDriver(DataDriver):
BASE_CAPABILITIES = (storage.Capabilities.DURABILITY,
storage.Capabilities.CLAIMS,
storage.Capabilities.AOD,
storage.Capabilities.HIGH_THROUGHPUT)
_COL_SUFIX = "_messages_fifo_p"
@decorators.lazy_property(write=False)
def message_controller(self):
controller = controllers.FIFOMessageController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_message_controller")(controller)
else:
return controller
class ControlDriver(storage.ControlDriverBase):
def __init__(self, conf, cache):
super(ControlDriver, self).__init__(conf, cache)
self.conf.register_opts(
drivers_management_store_mongodb.ALL_OPTS,
group=drivers_management_store_mongodb.GROUP_NAME)
self.mongodb_conf = self.conf[
drivers_management_store_mongodb.GROUP_NAME]
def close(self):
self.connection.close()
@decorators.lazy_property(write=False)
def connection(self):
"""MongoDB client connection instance."""
return _connection(self.mongodb_conf)
@decorators.lazy_property(write=False)
def database(self):
name = self.mongodb_conf.database
return self.connection[name]
@decorators.lazy_property(write=False)
def queues_database(self):
"""Database dedicated to the "queues" collection.
The queues collection is separated out into its own database
to avoid writer lock contention with the messages collections.
"""
name = self.mongodb_conf.database + '_queues'
return self.connection[name]
@decorators.lazy_property(write=False)
def topics_database(self):
"""Database dedicated to the "topics" collection.
The topics collection is separated out into its own database
to avoid writer lock contention with the messages collections.
"""
name = self.mongodb_conf.database + '_topics'
return self.connection[name]
@decorators.lazy_property(write=False)
def queue_controller(self):
controller = controllers.QueueController(self)
if (self.conf.profiler.enabled and
(self.conf.profiler.trace_message_store or
self.conf.profiler.trace_management_store)):
return profiler.trace_cls("mongodb_queues_controller")(controller)
else:
return controller
@property
def pools_controller(self):
controller = controllers.PoolsController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("mongodb_pools_controller")(controller)
else:
return controller
@property
def catalogue_controller(self):
controller = controllers.CatalogueController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("mongodb_catalogue_"
"controller")(controller)
else:
return controller
@property
def flavors_controller(self):
controller = controllers.FlavorsController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("mongodb_flavors_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def topic_controller(self):
controller = controllers.TopicController(self)
if (self.conf.profiler.enabled and
(self.conf.profiler.trace_message_store or
self.conf.profiler.trace_management_store)):
return profiler.trace_cls("mongodb_topics_controller")(controller)
else:
return controller